deepsu commited on
Commit
d98f7ad
·
1 Parent(s): 2730865

Fix: use git lfs for large model files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.json filter=lfs diff=lfs merge=lfs -text
37
+ models/** filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,31 @@
 
 
 
 
 
 
 
 
1
  ---
2
- title: HR TOOL V2
3
- emoji: 📊
4
- colorFrom: gray
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- short_description: HR tool to find best candidates for the role
 
 
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # 🧠 HR Tool Backend (AI Resume Intelligence System)
2
+
3
+ A production-ready AI-powered HR system that:
4
+ - Extracts structured resume data using **Qwen2.5-0.5B + LoRA**
5
+ - Matches resumes with job descriptions using **MiniLM embeddings**
6
+ - Provides semantic ATS scoring via **FastAPI backend**
7
+ - Designed for deployment on **Hugging Face Spaces / Docker**
8
+
9
  ---
10
+
11
+ # 🚀 Features
12
+
13
+ ### 📄 Resume Intelligence
14
+ - PDF → Clean text extraction
15
+ - LLM-based structured JSON generation
16
+ - Strict schema-based output (no hallucination design)
17
+
18
+ ### 🧠 Semantic Matching (ATS)
19
+ - Resume ↔ Job Description similarity
20
+ - Uses `sentence-transformers/all-MiniLM-L6-v2`
21
+ - Cosine similarity scoring (0–100 ATS score)
22
+
23
+ ### ⚡ FastAPI Backend
24
+ - Upload resume PDFs
25
+ - Get structured JSON output
26
+ - Compare with job descriptions
27
+ - Get ranking score
28
+
29
  ---
30
 
31
+ # 🏗️ System Architecture
main.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, Form
2
+ from typing import List
3
+ import json
4
+ import time
5
+ import os
6
+ import uuid
7
+
8
+ from modules.pdf_to_json import PDFToJSON
9
+ from modules.comparison import CompareData
10
+
11
+ app = FastAPI()
12
+
13
+ pdf_to_json = PDFToJSON()
14
+ compare = CompareData()
15
+
16
+ THRESHOLD = 0.675
17
+
18
+
19
+ # ---------------------------------------------
20
+ # HEALTH CHECK
21
+ # ---------------------------------------------
22
+ @app.get("/")
23
+ def home():
24
+ return {"status": "ATS API Running 🚀"}
25
+
26
+
27
+ # ---------------------------------------------
28
+ # MAIN MATCHING ENDPOINT
29
+ # ---------------------------------------------
30
+ @app.post("/match-resumes/")
31
+ async def match_resumes(
32
+ jd: str = Form(...),
33
+ files: List[UploadFile] = File(...)
34
+ ):
35
+ start_time = time.time()
36
+
37
+ print("\n================= NEW REQUEST =================\n")
38
+
39
+ # -----------------------------------------
40
+ # Parse JD
41
+ # -----------------------------------------
42
+ print("📌 Step 1: Parsing Job Description...")
43
+ try:
44
+ jd = json.loads(jd)
45
+ print("✅ JD parsed successfully\n")
46
+ except Exception as e:
47
+ print("❌ JD parsing failed")
48
+ return {"error": "Invalid JD format", "details": str(e)}
49
+
50
+ results = []
51
+
52
+ temp_dir = "./temp"
53
+ os.makedirs(temp_dir, exist_ok=True)
54
+
55
+ print(f"📥 Total resumes received: {len(files)}\n")
56
+
57
+ # -----------------------------------------
58
+ # PROCESS EACH RESUME
59
+ # -----------------------------------------
60
+ for idx, file in enumerate(files):
61
+
62
+ print(f"\n---------------------------------------------")
63
+ print(f"📄 Processing Resume {idx + 1}/{len(files)}: {file.filename}")
64
+ print(f"---------------------------------------------")
65
+
66
+ # ✅ UNIQUE FILE NAME (IMPORTANT FOR PUBLIC APIS)
67
+ file_path = os.path.join(temp_dir, f"{uuid.uuid4()}.pdf")
68
+
69
+ try:
70
+ # STEP 1: Save file
71
+ print("💾 Step 1: Saving file...")
72
+ with open(file_path, "wb") as f:
73
+ f.write(await file.read())
74
+ print("✅ File saved")
75
+
76
+ # STEP 2: Extract text
77
+ print("📄 Step 2: Extracting PDF text...")
78
+ resume_texts = pdf_to_json.extract_text([file_path])
79
+ print("✅ Text extracted")
80
+
81
+ # STEP 3: Convert to JSON
82
+ print("🧠 Step 3: Generating structured JSON...")
83
+ resume_json = pdf_to_json.generate_json(resume_texts)[0]
84
+ print("✅ JSON generated")
85
+
86
+ # STEP 4: Process text
87
+ print("⚙️ Step 4: Preparing embeddings input...")
88
+ processed_resume = compare.process_resume(resume_json)
89
+ processed_jd = compare.process_jd(jd)
90
+ print("✅ Text processed")
91
+
92
+ # STEP 5: Encoding
93
+ print("🔢 Step 5: Generating embeddings...")
94
+ resume_emb = compare.encode(processed_resume)
95
+ jd_emb = compare.encode(processed_jd)
96
+ print("✅ Embeddings created")
97
+
98
+ # STEP 6: Similarity
99
+ print("📊 Step 6: Calculating similarity score...")
100
+ score = compare.compare(resume_emb, jd_emb)
101
+ print(f"📊 Score: {score}")
102
+
103
+ # STEP 7: Name extraction
104
+ name = resume_json.get("name", "Unknown")
105
+
106
+ # STEP 8: Filter
107
+ if score >= THRESHOLD:
108
+ print("✅ Candidate QUALIFIED")
109
+ results.append({
110
+ "name": name,
111
+ "score": float(score),
112
+ "resume": resume_json
113
+ })
114
+ else:
115
+ print("❌ Candidate rejected (below threshold)")
116
+
117
+ finally:
118
+ # -----------------------------------------
119
+ # 🧹 DELETE FILE AFTER PROCESSING
120
+ # -----------------------------------------
121
+ if os.path.exists(file_path):
122
+ os.remove(file_path)
123
+ print(f"🧹 Deleted temp file")
124
+
125
+ # ---------------------------------------------
126
+ # FINAL RESPONSE
127
+ # ---------------------------------------------
128
+ end_time = time.time()
129
+
130
+ print("\n================= PROCESS COMPLETE =================\n")
131
+
132
+ return {
133
+ "threshold": THRESHOLD,
134
+ "total_matched": len(results),
135
+ "processing_time_seconds": round(end_time - start_time, 2),
136
+ "candidates": results
137
+ }
models/mnr_1/1_Pooling/config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bb80272f7adad76ce93073c25e18113ee66be1e91f60bcdd72aa41d05b28247e
3
+ size 94
models/mnr_1/README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:033d5318e3d7d204f46d47b7418bf5ab8b22f0d4ea0228fdf447887dcfedb827
3
+ size 34954
models/mnr_1/config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6e48034f60114d869b2a3e418d67af0220004a228e6db788af845503c4c14465
3
+ size 774
models/mnr_1/config_sentence_transformers.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:70f27f0e7e4ba5617be48ae690066b2c237dad02474162b4c11bbcec1c6b7d9d
3
+ size 296
models/mnr_1/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b4676b573b8c272d1383fee4b0aa61ed07c91f0208250e8e5ef0e3ed1ecbaf79
3
+ size 90864192
models/mnr_1/modules.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5286260f53f0f5d318eb02f1565fd30f7ec7c6d305f41ad29e2bb3ec974046a6
3
+ size 448
models/mnr_1/sentence_bert_config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5fee3fcdc4a727fad422f0fed0c867b0921eefa9051de221521956ed42eb4c19
3
+ size 250
models/mnr_1/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:851ca67100d372ca3ae031a6abd168f53489eebfd7d89523f35c5c9b4d372c3c
3
+ size 711649
models/mnr_1/tokenizer_config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e9becfd25977397187a9f9359494ed9682d6f93bb58dda91da6cb36e02ef770e
3
+ size 618
models/qwen-2.5-v2/README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fd061e0e548c4f35c292a198bbf8b94ff5e571c568474aa4fc08b8b806de09c7
3
+ size 1518
models/qwen-2.5-v2/checkpoint-1010/README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e1f8400c63a37fa69b4a09c9e7053061e40b2fe3e8951e5a8a1767b375b791ea
3
+ size 5268
models/qwen-2.5-v2/checkpoint-1010/adapter_config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:43a3c15cc9bfdc8c64fb67bd97850c0587a5e014c5b761f9c01eaba09e9482f0
3
+ size 1311
models/qwen-2.5-v2/checkpoint-1010/adapter_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8ba09c8697ba3535b4c56bb91d91acd5eb572bc0cc86c8013a8044b7150ec026
3
+ size 17640136
models/qwen-2.5-v2/checkpoint-1010/chat_template.jinja ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8aa40ce145adb73cb3a75194dc0224702a95850ec5275cabb728496bbd749fc6
3
+ size 2561
models/qwen-2.5-v2/checkpoint-1010/optimizer.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a4e5e181469198b65485d25184ebfb1e51b34c4de79c3a93ce332df2521d62d0
3
+ size 35479763
models/qwen-2.5-v2/checkpoint-1010/rng_state.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1bf37342a4e2004e2b59ecd956410c6d91e61253e6fd30356e13956f4c914499
3
+ size 14645
models/qwen-2.5-v2/checkpoint-1010/scheduler.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca3c880af772c4eb36603de9b9924cc5e93c3999fa5c27fa211190ead91c940d
3
+ size 1465
models/qwen-2.5-v2/checkpoint-1010/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:01c060135db283a7f3530c39cddd55bbac96eeb2ba8a6da6c8a103f47c8be824
3
+ size 11422455
models/qwen-2.5-v2/checkpoint-1010/tokenizer_config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b2c7a957149820032339cd91546b9b919ef32f9dd1f36dfcb9c65526364a22b7
3
+ size 4764
models/qwen-2.5-v2/checkpoint-1010/trainer_state.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:00464c5228b2d6f8ae37eda5bfe7225c8d4ec39df826d8b6f5bb14f28de43b32
3
+ size 19475
models/qwen-2.5-v2/checkpoint-1010/training_args.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:43f28af2137e96a50c528960dfb51cc8bb17eb7510646a7d6e556c54bb882ce2
3
+ size 5585
modules/comparison.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sentence_transformers import SentenceTransformer
2
+ from pathlib import Path
3
+ import torch
4
+ import torch.nn.functional as F
5
+
6
+
7
+ model = None
8
+ def get_model(base_path):
9
+ global model
10
+ if model is None:
11
+ from sentence_transformers import SentenceTransformer
12
+ model = SentenceTransformer(base_path)
13
+ return model
14
+
15
+ class CompareData:
16
+ """
17
+ Converts resume and job description JSON into embeddings
18
+ and compares them using PyTorch cosine similarity.
19
+ """
20
+
21
+ def __init__(self, model_path="models/mnr_1"):
22
+ # ---------------------------------------------
23
+ # Resolve absolute path safely
24
+ # ---------------------------------------------
25
+ base_dir = Path(__file__).resolve().parents[1]
26
+ absolute_model_path = base_dir / model_path
27
+
28
+ print("📦 Loading model from:", absolute_model_path)
29
+
30
+ # Load SentenceTransformer model
31
+ self.model = get_model(str(absolute_model_path))
32
+
33
+ # ---------------------------------------------
34
+ # RESUME PROCESSING
35
+ # ---------------------------------------------
36
+ def process_resume(self, resume_json):
37
+ resume_text = f"""
38
+ [RESUME]
39
+ Role: {resume_json.get("role", "")}
40
+
41
+ Key Skills: {", ".join(resume_json.get("skills", []))}
42
+ Skills: {", ".join(resume_json.get("skills", []))}
43
+
44
+ Experience: {resume_json.get("years_of_experience", 0)} years
45
+
46
+ Work Experience:
47
+ {" . ".join(resume_json.get("work_experience", []))}
48
+
49
+ Education:
50
+ {", ".join(resume_json.get("education", []))}
51
+ """
52
+ return resume_text.strip()
53
+
54
+ # ---------------------------------------------
55
+ # JOB DESCRIPTION PROCESSING
56
+ # ---------------------------------------------
57
+ def process_jd(self, jd_json):
58
+ jd_text = f"""
59
+ [JOB]
60
+ Role: {jd_json.get("job_title", "")}
61
+
62
+ Key Skills: {", ".join(jd_json.get("required_skills", []))}
63
+ Skills: {", ".join(jd_json.get("required_skills", []))}
64
+
65
+ Requirements:
66
+ {" . ".join(jd_json.get("requirements", []))}
67
+
68
+ Responsibilities:
69
+ {" . ".join(jd_json.get("responsibilities", []))}
70
+
71
+ Education:
72
+ {", ".join(jd_json.get("education", []))}
73
+ """
74
+ return jd_text.strip()
75
+
76
+ # ---------------------------------------------
77
+ # EMBEDDING GENERATION
78
+ # ---------------------------------------------
79
+ def encode(self, text):
80
+ """
81
+ Convert text into embedding tensor.
82
+ """
83
+ return torch.tensor(
84
+ self.model.encode(
85
+ text,
86
+ convert_to_numpy=True,
87
+ normalize_embeddings=True
88
+ ),
89
+ dtype=torch.float32
90
+ )
91
+
92
+ # ---------------------------------------------
93
+ # SIMILARITY CHECK (PYTORCH)
94
+ # ---------------------------------------------
95
+ def compare(self, resume_embedding, jd_embedding):
96
+ """
97
+ Compute cosine similarity using PyTorch.
98
+ """
99
+
100
+ # Ensure correct shape: [1, dim]
101
+ resume_embedding = resume_embedding.unsqueeze(0) if resume_embedding.dim() == 1 else resume_embedding
102
+ jd_embedding = jd_embedding.unsqueeze(0) if jd_embedding.dim() == 1 else jd_embedding
103
+
104
+ similarity = F.cosine_similarity(
105
+ resume_embedding,
106
+ jd_embedding
107
+ )
108
+
109
+ return similarity.item()
modules/pdf_to_json.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import torch
5
+ from pathlib import Path
6
+ from pypdf import PdfReader
7
+ from transformers import AutoTokenizer, AutoModelForCausalLM
8
+ from peft import PeftModel
9
+
10
+ # =========================================================
11
+ # ⚙️ CPU OPTIMIZATION (IMPORTANT for HF Spaces)
12
+ # =========================================================
13
+ torch.set_num_threads(max(1, os.cpu_count() - 1))
14
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
15
+
16
+
17
+ # =========================================================
18
+ # 🧼 TEXT CLEANING
19
+ # =========================================================
20
+ def clean_resume_text(text: str) -> str:
21
+ text = re.sub(r"[\x00-\x1F\x7F]", " ", text)
22
+ text = text.replace("•", "-").replace("●", "-").replace("▪", "-")
23
+ text = re.sub(r"[ \t]+", " ", text)
24
+ text = re.sub(r"\n{3,}", "\n\n", text)
25
+ return text.strip()
26
+
27
+
28
+ # =========================================================
29
+ # 🧠 STRUCTURE ENFORCEMENT
30
+ # =========================================================
31
+ def enforce_structure(text: str) -> str:
32
+ sections = ["ABOUT", "EXPERIENCE", "EDUCATION", "SKILLS", "PROJECTS", "CERTIFICATIONS"]
33
+
34
+ for s in sections:
35
+ text = re.sub(
36
+ rf"\b{s}\b",
37
+ f"\n\n===== {s} =====\n",
38
+ text,
39
+ flags=re.I
40
+ )
41
+
42
+ return text
43
+
44
+
45
+ # =========================================================
46
+ # 🧾 PROMPT BUILDER
47
+ # =========================================================
48
+ def build_prompt(input_text: str) -> str:
49
+ schema = {
50
+ "name": None,
51
+ "email": None,
52
+ "location": None,
53
+ "phone": None,
54
+ "linkedin": None,
55
+ "github": None,
56
+ "work_experience": [],
57
+ "skills": [],
58
+ "projects": [],
59
+ "education": [],
60
+ "certifications": []
61
+ }
62
+
63
+ return f"""<|im_start|>system
64
+ You are a strict JSON extraction engine for resumes.
65
+
66
+ RULES:
67
+ - Output ONLY valid JSON
68
+ - No explanation
69
+ - No markdown
70
+ - Follow schema exactly
71
+ - Use null if missing
72
+ - Use [] for missing lists
73
+ <|im_end|>
74
+
75
+ <|im_start|>user
76
+ Extract resume data strictly.
77
+
78
+ Schema:
79
+ {json.dumps(schema, indent=2)}
80
+
81
+ RESUME:
82
+ {input_text}
83
+ <|im_end|>
84
+ """
85
+
86
+
87
+ # =========================================================
88
+ # 🤖 MODEL LOADER (SAFE + LAZY + HF FRIENDLY)
89
+ # =========================================================
90
+ class ModelLoader:
91
+ def __init__(self, base_model_name, adapter_path):
92
+ self.base_model_name = base_model_name
93
+ self.adapter_path = adapter_path
94
+ self.model = None
95
+ self.tokenizer = None
96
+
97
+ def load(self):
98
+ if self.model is None:
99
+
100
+ # Load tokenizer
101
+ self.tokenizer = AutoTokenizer.from_pretrained(self.base_model_name)
102
+ self.tokenizer.pad_token = self.tokenizer.eos_token
103
+
104
+ # Load base model
105
+ base_model = AutoModelForCausalLM.from_pretrained(
106
+ self.base_model_name,
107
+ torch_dtype=torch.float32,
108
+ device_map="cpu"
109
+ )
110
+
111
+ # Attach LoRA adapter
112
+ self.model = PeftModel.from_pretrained(base_model, self.adapter_path)
113
+ self.model.eval()
114
+
115
+ print("✅ Model Loaded Successfully")
116
+
117
+ return self.model, self.tokenizer
118
+
119
+
120
+ # =========================================================
121
+ # 📄 MAIN PIPELINE CLASS
122
+ # =========================================================
123
+ class PDFToJSON:
124
+
125
+ def __init__(self):
126
+ base_model_name = "Qwen/Qwen2.5-0.5B-Instruct"
127
+
128
+ adapter_path = os.path.abspath(
129
+ os.path.join(
130
+ os.path.dirname(__file__),
131
+ "../models/qwen-2.5-v2/checkpoint-1010"
132
+ )
133
+ )
134
+
135
+ self.loader = ModelLoader(base_model_name, adapter_path)
136
+
137
+ # -----------------------------------------------------
138
+ # 📄 PDF → TEXT
139
+ # -----------------------------------------------------
140
+ def extract_text(self, resume_pdfs):
141
+ cleaned_resumes = []
142
+
143
+ for pdf in resume_pdfs:
144
+ reader = PdfReader(pdf)
145
+ text_pages = []
146
+
147
+ for page in reader.pages:
148
+ text = page.extract_text() or ""
149
+ if text.strip():
150
+ text_pages.append(text)
151
+
152
+ full_text = "\n".join(text_pages)
153
+ cleaned = clean_resume_text(full_text)
154
+ structured = enforce_structure(cleaned)
155
+
156
+ cleaned_resumes.append(structured)
157
+
158
+ return cleaned_resumes
159
+
160
+ # -----------------------------------------------------
161
+ # 🧠 TEXT → JSON
162
+ # -----------------------------------------------------
163
+ def generate_json(self, resumes):
164
+ model, tokenizer = self.loader.load()
165
+ results = []
166
+
167
+ for resume in resumes:
168
+
169
+ prompt = build_prompt(resume)
170
+
171
+ inputs = tokenizer(
172
+ prompt,
173
+ return_tensors="pt",
174
+ truncation=True,
175
+ max_length=1800,
176
+ padding=True
177
+ )
178
+
179
+ with torch.no_grad():
180
+ output = model.generate(
181
+ **inputs,
182
+ max_new_tokens=500,
183
+ do_sample=False,
184
+ eos_token_id=tokenizer.eos_token_id,
185
+ repetition_penalty=1.05,
186
+ )
187
+
188
+ input_len = inputs["input_ids"].shape[1]
189
+ output = output[0][input_len:]
190
+
191
+ response = tokenizer.decode(output, skip_special_tokens=True)
192
+
193
+ # -------------------------------------------------
194
+ # 🧾 SAFE JSON PARSING
195
+ # -------------------------------------------------
196
+ try:
197
+ match = re.search(r"\{[\s\S]*\}", response)
198
+
199
+ if not match:
200
+ raise ValueError("No JSON found in output")
201
+
202
+ parsed = json.loads(match.group())
203
+ results.append(parsed)
204
+
205
+ except Exception as e:
206
+ results.append({
207
+ "error": str(e),
208
+ "raw_output": response[:500]
209
+ })
210
+
211
+ return results
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+
5
+ pandas
6
+ scikit-learn
7
+ pypdf
8
+
9
+ torch
10
+ transformers
11
+ sentence-transformers
12
+ peft