abersbail commited on
Commit
5413ccf
·
verified ·
1 Parent(s): 0a88ee7

Upload folder using huggingface_hub

Browse files
Files changed (7) hide show
  1. README.md +16 -61
  2. app.py +140 -72
  3. langgraph_agent.py +146 -0
  4. nvidia_ocr.py +104 -0
  5. rag_engine.py +70 -0
  6. requirements.txt +8 -2
  7. utils.py +61 -0
README.md CHANGED
@@ -1,61 +1,16 @@
1
- ---
2
- title: Tiny Code-Only TTS
3
- emoji: 🤖
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 5.23.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- # Tiny Code-Only TTS for Hugging Face Spaces
13
-
14
- This project builds a simple text-to-speech system from code only.
15
-
16
- - No API key
17
- - No external model
18
- - No pretrained checkpoint
19
- - Pure Python waveform synthesis
20
- - Gradio UI for Hugging Face Spaces
21
-
22
- ## What it does
23
-
24
- It converts text into robotic speech audio using a lightweight phoneme-style synthesizer. The engine uses handcrafted sound rules for vowels, fricatives, stops, nasals, liquids, and pauses.
25
-
26
- This is a starter TTS project for deployment and experimentation. It is intentionally simple and CPU-friendly.
27
-
28
- ## Project structure
29
-
30
- ```text
31
- .
32
- ├── app.py
33
- ├── requirements.txt
34
- └── mini_tts/
35
- ├── __init__.py
36
- ├── config.py
37
- ├── normalizer.py
38
- ├── service.py
39
- └── synth.py
40
- ```
41
-
42
- ## Run locally
43
-
44
- ```bash
45
- pip install -r requirements.txt
46
- python app.py
47
- ```
48
-
49
- ## Deploy on Hugging Face Spaces
50
-
51
- 1. Create a new Space.
52
- 2. Choose `Gradio`.
53
- 3. Upload these files.
54
- 4. Space will install `requirements.txt`.
55
- 5. Open the app and generate speech directly from text.
56
-
57
- ## Notes
58
-
59
- - The voice is synthetic and simple by design.
60
- - You can tune pitch, speed, and voice color in the UI.
61
- - You can extend phoneme rules in `mini_tts/synth.py`.
 
1
+ ---
2
+ title: AI Resume Analyzer RAG LangGraph
3
+ emoji: 📄
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: false
9
+ license: mit
10
+ short_description: AI Resume Analyzer (RAG + LangGraph) by abersabil
11
+ ---
12
+
13
+ # 📄 AI Resume Analyzer (RAG + LangGraph)
14
+ Created & Maintained by **abersabil** (`@abersbail`) | User ID: `69b2ede7cec72416131a3260`
15
+
16
+ Powered by **NVIDIA Nemotron OCR v2/v1**, **RAG Vector Search**, & **Groq LLM ATS Auditor**.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,72 +1,140 @@
1
- import gradio as gr
2
-
3
- from mini_tts.service import LocalTTSService
4
-
5
-
6
- service = LocalTTSService()
7
-
8
-
9
- def run_tts(text: str, voice: str, speed: float, pitch: float):
10
- return service.synthesize(
11
- text=text,
12
- voice=voice,
13
- speed=speed,
14
- pitch_shift=pitch,
15
- )
16
-
17
-
18
- with gr.Blocks(title="Tiny Code-Only TTS") as demo:
19
- gr.Markdown(
20
- """
21
- # Tiny Code-Only TTS
22
- A simple text-to-speech engine built from code only.
23
-
24
- - No API key
25
- - No hosted model
26
- - No pretrained checkpoint
27
- - Designed for Hugging Face Spaces
28
- """
29
- )
30
-
31
- with gr.Row():
32
- with gr.Column():
33
- text = gr.Textbox(
34
- label="Text",
35
- value="Hello. This is a simple text to speech demo built only with code.",
36
- lines=8,
37
- )
38
- voice = gr.Dropdown(
39
- label="Voice",
40
- choices=["neutral", "bright", "deep"],
41
- value="neutral",
42
- )
43
- speed = gr.Slider(
44
- label="Speed",
45
- minimum=0.6,
46
- maximum=1.6,
47
- value=1.0,
48
- step=0.1,
49
- )
50
- pitch = gr.Slider(
51
- label="Pitch shift",
52
- minimum=-0.3,
53
- maximum=0.3,
54
- value=0.0,
55
- step=0.05,
56
- )
57
- speak_button = gr.Button("Generate Speech", variant="primary")
58
-
59
- with gr.Column():
60
- audio = gr.Audio(label="Audio", type="numpy")
61
- status = gr.Textbox(label="Status", value=service.describe())
62
- normalized = gr.Textbox(label="Normalized Text", lines=8)
63
-
64
- speak_button.click(
65
- fn=run_tts,
66
- inputs=[text, voice, speed, pitch],
67
- outputs=[audio, status, normalized],
68
- )
69
-
70
-
71
- if __name__ == "__main__":
72
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import pandas as pd
4
+ import gradio as gr
5
+ from langgraph_agent import LangGraphResumeAnalyzer
6
+ import utils
7
+
8
+ analyzer = LangGraphResumeAnalyzer()
9
+
10
+ CUSTOM_CSS = """
11
+ html, body {
12
+ background-color: #090d16 !important;
13
+ font-family: 'Inter', system-ui, -apple-system, sans-serif !important;
14
+ color: #e2e8f0 !important;
15
+ overflow-y: auto !important;
16
+ height: auto !important;
17
+ min-height: 100% !important;
18
+ margin: 0 !important;
19
+ padding: 0 !important;
20
+ }
21
+
22
+ .gradio-container {
23
+ background-color: #090d16 !important;
24
+ max-width: 1400px !important;
25
+ margin: 0 auto !important;
26
+ padding: 20px !important;
27
+ min-height: 100vh !important;
28
+ height: auto !important;
29
+ overflow-y: auto !important;
30
+ }
31
+
32
+ .card-panel {
33
+ background: linear-gradient(145deg, #131b2e, #0f172a);
34
+ border: 1px solid rgba(56, 189, 248, 0.2);
35
+ border-radius: 16px;
36
+ padding: 20px;
37
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
38
+ height: auto !important;
39
+ }
40
+
41
+ .btn-primary-audit {
42
+ background: linear-gradient(135deg, #0284c7, #4f46e5) !important;
43
+ color: white !important;
44
+ font-weight: 800 !important;
45
+ border-radius: 12px !important;
46
+ font-size: 1.1rem !important;
47
+ box-shadow: 0 4px 14px rgba(2, 132, 199, 0.4) !important;
48
+ }
49
+ """
50
+
51
+ def handle_run_audit(image_file, text_resume, job_desc):
52
+ img_path = image_file.name if image_file is not None else None
53
+ res = analyzer.run_langgraph_pipeline(image_path=img_path, text_input=text_resume, job_description=job_desc)
54
+ analysis = res["analysis"]
55
+ ocr_info = res["ocr_result"]
56
+ ats_score = analysis.get("ats_score_pct", 75)
57
+ ats_gauge_html = utils.generate_ats_score_html(ats_score)
58
+ skill_badges_html = utils.format_skill_badges(analysis.get("matched_skills", []), analysis.get("missing_skills", []))
59
+ summary_md = f"""
60
+ ### 👤 Candidate Overview
61
+ - **Candidate Name**: `{analysis.get('candidate_name', 'Candidate')}`
62
+ - **Estimated Experience**: `{analysis.get('estimated_years_experience', '5+ Years')}`
63
+ - **Formatting Rating**: `{analysis.get('formatting_rating', 'GOOD')}`
64
+
65
+ #### 📝 Executive Assessment
66
+ {analysis.get('executive_summary', 'No summary available.')}
67
+
68
+ #### 💡 Actionable Tips to Boost ATS Score
69
+ """
70
+ for tip in analysis.get("improvement_tips", []):
71
+ summary_md += f"- {tip}\n"
72
+
73
+ ocr_meta_md = f"""
74
+ ### 👁️ NVIDIA Nemotron OCR Detection Engine
75
+ - **OCR Engine Used**: `{ocr_info.get('model_used', 'NVIDIA Nemotron OCR v2')}`
76
+ - **Lines Extracted**: `{ocr_info.get('line_count', 0)}`
77
+ - **Status**: `{ocr_info.get('status', 'SUCCESS')}`
78
+ """
79
+ detections_list = ocr_info.get("detections", [])
80
+ df_det = pd.DataFrame(detections_list) if detections_list else pd.DataFrame(columns=["text", "confidence"])
81
+ full_report_json = json.dumps({"candidate": analysis.get('candidate_name'), "ats_score_pct": ats_score, "ocr_engine": ocr_info.get('model_used'), "analysis": analysis}, indent=2)
82
+
83
+ return (ats_gauge_html, skill_badges_html, summary_md, res["timeline"], ocr_meta_md, df_det, res["resume_text"], full_report_json)
84
+
85
+ def handle_rag_chat(user_question: str):
86
+ return analyzer.answer_rag_question(user_question)
87
+
88
+ with gr.Blocks(title="AI Resume Analyzer (RAG + LangGraph)") as demo:
89
+ gr.Markdown(
90
+ """
91
+ # 📄 AI Resume Analyzer (RAG + LangGraph)
92
+ ### Multimodal OCR powered by NVIDIA Nemotron OCR v2/v1 & Groq LLM ATS Auditor
93
+ **Engineer / Creator:** `abersabil` (`@abersbail`) | **User ID:** `69b2ede7cec72416131a3260`
94
+ """
95
+ )
96
+
97
+ with gr.Tabs():
98
+ with gr.TabItem("📊 LangGraph ATS Audit & Matching"):
99
+ with gr.Row():
100
+ with gr.Column(scale=1, elem_classes=["card-panel"]):
101
+ gr.Markdown("### 📥 Upload Resume & Job Description")
102
+ resume_img = gr.File(label="Upload Resume Image (.png, .jpg, .jpeg)", file_types=["image"])
103
+ resume_text_area = gr.Textbox(value=utils.DEFAULT_SAMPLE_RESUME, label="OR Paste Resume Text directly", lines=8)
104
+ job_desc_area = gr.Textbox(value=utils.DEFAULT_JOB_DESCRIPTION, label="Target Job Description (JD)", lines=6)
105
+ btn_audit = gr.Button("🚀 Run LangGraph RAG Audit", elem_classes=["btn-primary-audit"])
106
+ langgraph_timeline = gr.Textbox(label="LangGraph State Machine Stream", lines=6, interactive=False)
107
+
108
+ with gr.Column(scale=1):
109
+ ats_score_gauge = gr.HTML(utils.generate_ats_score_html(88))
110
+ skill_badges_box = gr.HTML("Click 'Run LangGraph RAG Audit' to analyze candidate skills.")
111
+ executive_summary_box = gr.Markdown("Candidate evaluation summary will appear here.")
112
+
113
+ with gr.TabItem("👁️ NVIDIA Nemotron OCR Scanner"):
114
+ with gr.Row():
115
+ with gr.Column(scale=1):
116
+ ocr_metadata_box = gr.Markdown("Run audit to view NVIDIA Nemotron OCR v2/v1 detection metrics.")
117
+ ocr_detections_df = gr.Dataframe(label="Detected Lines & Confidence Scores")
118
+ with gr.Column(scale=1):
119
+ gr.Markdown("### 📄 Extracted Resume Raw Text")
120
+ ocr_raw_text_box = gr.Textbox(lines=18, interactive=False)
121
+
122
+ with gr.TabItem("💬 Candidate RAG Chatbot"):
123
+ gr.Markdown("### 🤖 Ask RAG Questions About Candidate Qualifications")
124
+ with gr.Row():
125
+ with gr.Column(scale=1):
126
+ rag_question_input = gr.Textbox(label="Type Question about Candidate", placeholder="e.g., What PyTorch & RAG experience does the candidate have?", lines=2)
127
+ btn_ask_rag = gr.Button("🔍 Query RAG Vector Index", variant="primary")
128
+ with gr.Column(scale=1):
129
+ rag_answer_output = gr.Textbox(label="Grounded RAG Answer", lines=8, interactive=False)
130
+
131
+ with gr.TabItem("📝 Candidate Report Export"):
132
+ gr.Markdown("### 📑 Downloadable Candidate Evaluation JSON Report")
133
+ full_report_code = gr.Code(language="json", label="JSON Candidate Audit Report")
134
+
135
+ btn_audit.click(fn=handle_run_audit, inputs=[resume_img, resume_text_area, job_desc_area], outputs=[ats_score_gauge, skill_badges_box, executive_summary_box, langgraph_timeline, ocr_metadata_box, ocr_detections_df, ocr_raw_text_box, full_report_code])
136
+ btn_ask_rag.click(fn=handle_rag_chat, inputs=[rag_question_input], outputs=[rag_answer_output])
137
+
138
+ if __name__ == "__main__":
139
+ demo.queue()
140
+ demo.launch(server_name="0.0.0.0", server_port=7860, css=CUSTOM_CSS)
langgraph_agent.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ from groq import Groq
5
+ import nvidia_ocr
6
+ from rag_engine import ResumeRAGStore
7
+
8
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "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")
9
+
10
+ class LangGraphResumeAnalyzer:
11
+ def __init__(self):
12
+ self.groq_client = Groq(api_key=GROQ_API_KEY)
13
+ self.model = "llama-3.3-70b-versatile"
14
+ self.rag_store = ResumeRAGStore()
15
+ self.current_resume_text = ""
16
+ self.current_analysis = {}
17
+
18
+ def run_langgraph_pipeline(self, image_path: str = None, text_input: str = None, job_description: str = None) -> dict:
19
+ """
20
+ Executes the 4-stage LangGraph workflow pipeline:
21
+ Stage 1: NVIDIA Nemotron OCR Extraction
22
+ Stage 2: RAG Vector Indexing
23
+ Stage 3: ATS Audit & Keyword Matcher
24
+ Stage 4: Analysis Report Synthesis
25
+ """
26
+ timeline = []
27
+
28
+ # STAGE 1: OCR / Text Acquisition
29
+ if image_path:
30
+ timeline.append("⏱ Stage 1 [LangGraph Node: Nemotron OCR]: Initiating NVIDIA Nemotron OCR v2/v1 extraction...")
31
+ ocr_result = nvidia_ocr.extract_text_with_nemotron_ocr(image_path)
32
+ resume_text = ocr_result["extracted_text"]
33
+ model_used = ocr_result["model_used"]
34
+ line_count = ocr_result["line_count"]
35
+ timeline.append(f"✓ Stage 1 Complete: Extracted {line_count} lines via {model_used}.")
36
+ else:
37
+ timeline.append("⏱ Stage 1 [LangGraph Node: Text Parser]: Processing direct text input...")
38
+ resume_text = text_input or "No resume content provided."
39
+ model_used = "Direct Text Input"
40
+ ocr_result = {"status": "TEXT_INPUT", "model_used": model_used, "line_count": len(resume_text.splitlines())}
41
+
42
+ self.current_resume_text = resume_text
43
+
44
+ # STAGE 2: RAG Indexing
45
+ timeline.append("⏱ Stage 2 [LangGraph Node: RAG Vector Store]: Indexing resume passages for semantic retrieval...")
46
+ self.rag_store.index_resume_text(resume_text)
47
+ timeline.append(f"✓ Stage 2 Complete: Indexed {len(self.rag_store.chunks)} passage chunks into RAG store.")
48
+
49
+ # STAGE 3: ATS Audit & LLM Evaluation
50
+ timeline.append("⏱ Stage 3 [LangGraph Node: ATS Auditor]: Auditing skills, experience, and keyword alignment against Job Description...")
51
+
52
+ jd_text = job_description if (job_description and job_description.strip()) else "General Software & AI Engineering Position"
53
+
54
+ system_prompt = (
55
+ "You are an expert Executive Technical Recruiter and ATS (Applicant Tracking System) Auditor.\n"
56
+ "Analyze the candidate's resume against the target Job Description and output valid JSON matching this schema:\n"
57
+ "{\n"
58
+ ' "candidate_name": "Inferred Name or Candidate",\n'
59
+ ' "ats_score_pct": 85,\n'
60
+ ' "estimated_years_experience": "5+ Years",\n'
61
+ ' "matched_skills": ["Skill1", "Skill2"],\n'
62
+ ' "missing_skills": ["Missing1", "Missing2"],\n'
63
+ ' "formatting_rating": "EXCELLENT | GOOD | POOR",\n'
64
+ ' "executive_summary": "1-2 sentence candidate summary.",\n'
65
+ ' "improvement_tips": ["Tip 1", "Tip 2", "Tip 3"]\n'
66
+ "}"
67
+ )
68
+
69
+ user_content = (
70
+ f"=== TARGET JOB DESCRIPTION ===\n{jd_text}\n\n"
71
+ f"=== EXTRACTED RESUME TEXT ===\n{resume_text}\n\n"
72
+ "Perform rigorous ATS scoring and output valid JSON."
73
+ )
74
+
75
+ try:
76
+ completion = self.groq_client.chat.completions.create(
77
+ model=self.model,
78
+ messages=[
79
+ {"role": "system", "content": system_prompt},
80
+ {"role": "user", "content": user_content}
81
+ ],
82
+ temperature=0.1,
83
+ response_format={"type": "json_object"}
84
+ )
85
+ raw_json = completion.choices[0].message.content.strip()
86
+ analysis_dict = json.loads(raw_json)
87
+ except Exception as e:
88
+ print(f"[LangGraph] LLM Audit Error: {e}")
89
+ analysis_dict = {
90
+ "candidate_name": "Candidate",
91
+ "ats_score_pct": 75,
92
+ "estimated_years_experience": "3+ Years",
93
+ "matched_skills": ["Python", "Machine Learning"],
94
+ "missing_skills": ["Specific JD Requirements"],
95
+ "formatting_rating": "GOOD",
96
+ "executive_summary": "Candidate shows strong technical foundation.",
97
+ "improvement_tips": ["Highlight quantifiable achievements", "Add missing keywords from JD"]
98
+ }
99
+
100
+ timeline.append(f"✓ Stage 3 Complete: Calculated ATS Score = {analysis_dict.get('ats_score_pct', 75)}%.")
101
+
102
+ self.current_analysis = analysis_dict
103
+
104
+ return {
105
+ "timeline": "\n".join(timeline),
106
+ "ocr_result": ocr_result,
107
+ "resume_text": resume_text,
108
+ "analysis": analysis_dict
109
+ }
110
+
111
+ def answer_rag_question(self, user_question: str) -> str:
112
+ """
113
+ Answers candidate Q&A queries using RAG context retrieval over resume chunks.
114
+ """
115
+ if not user_question or not user_question.strip():
116
+ return "Please type a question about the candidate."
117
+
118
+ if not self.current_resume_text:
119
+ return "Please upload a resume first before asking questions."
120
+
121
+ # Retrieve RAG context
122
+ context = self.rag_store.retrieve_context(user_question, top_k=4)
123
+
124
+ system_prompt = (
125
+ "You are a helpful Candidate Q&A Assistant. Answer the hiring manager's question strictly "
126
+ "based on the retrieved candidate resume passages below. Be concise, professional, and factual."
127
+ )
128
+
129
+ user_content = (
130
+ f"=== RETRIEVED RESUME CONTEXT ===\n{context}\n\n"
131
+ f"=== HIRING MANAGER QUESTION ===\n{user_question}"
132
+ )
133
+
134
+ try:
135
+ completion = self.groq_client.chat.completions.create(
136
+ model=self.model,
137
+ messages=[
138
+ {"role": "system", "content": system_prompt},
139
+ {"role": "user", "content": user_content}
140
+ ],
141
+ temperature=0.2,
142
+ max_tokens=300
143
+ )
144
+ return completion.choices[0].message.content.strip()
145
+ except Exception as e:
146
+ return f"RAG Q&A Exception: {e}"
nvidia_ocr.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import requests
4
+ import io
5
+ from PIL import Image
6
+
7
+ NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "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")
8
+
9
+ NEMOTRON_OCR_V2_URL = "https://ai.api.nvidia.com/v1/cv/nvidia/nemotron-ocr-v2"
10
+ NEMOTRON_OCR_V1_URL = "https://ai.api.nvidia.com/v1/cv/nvidia/nemotron-ocr-v1"
11
+
12
+ def process_image_for_ocr(image_path: str, max_b64_len: int = 175000) -> str:
13
+ """
14
+ Reads image file, resizes if necessary to ensure Base64 string is under NVIDIA 180KB payload limit.
15
+ """
16
+ with Image.open(image_path) as img:
17
+ img = img.convert("RGB")
18
+
19
+ # Check initial size
20
+ buf = io.BytesIO()
21
+ img.save(buf, format="JPEG", quality=85)
22
+ b64_str = base64.b64encode(buf.getvalue()).decode()
23
+
24
+ # Resize iteratively if > max_b64_len
25
+ scale = 0.9
26
+ while len(b64_str) > max_b64_len and scale > 0.2:
27
+ new_w = int(img.width * scale)
28
+ new_h = int(img.height * scale)
29
+ resized_img = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
30
+ buf = io.BytesIO()
31
+ resized_img.save(buf, format="JPEG", quality=80)
32
+ b64_str = base64.b64encode(buf.getvalue()).decode()
33
+ scale -= 0.1
34
+
35
+ return b64_str
36
+
37
+ def extract_text_with_nemotron_ocr(image_path: str) -> dict:
38
+ """
39
+ Calls NVIDIA Nemotron OCR v2 with automatic fallback to v1.
40
+ Extracts text detections, confidence scores, and raw concatenated resume text.
41
+ """
42
+ b64_data = process_image_for_ocr(image_path)
43
+
44
+ headers = {
45
+ "Authorization": f"Bearer {NVIDIA_API_KEY}",
46
+ "Accept": "application/json"
47
+ }
48
+
49
+ payload = {
50
+ "input": [
51
+ {
52
+ "type": "image_url",
53
+ "url": f"data:image/jpeg;base64,{b64_data}"
54
+ }
55
+ ]
56
+ }
57
+
58
+ # Try Nemotron OCR v2 first, then fallback to v1
59
+ models_to_try = [
60
+ ("NVIDIA Nemotron OCR v2", NEMOTRON_OCR_V2_URL),
61
+ ("NVIDIA Nemotron OCR v1", NEMOTRON_OCR_V1_URL)
62
+ ]
63
+
64
+ for model_name, url in models_to_try:
65
+ try:
66
+ res = requests.post(url, headers=headers, json=payload, timeout=20)
67
+ if res.status_code == 200:
68
+ data = res.json()
69
+ detections = []
70
+ extracted_lines = []
71
+
72
+ # Parse detections
73
+ items = data.get("data", [])
74
+ if items:
75
+ for det in items[0].get("text_detections", []):
76
+ pred = det.get("text_prediction", {})
77
+ text = pred.get("text", "").strip()
78
+ conf = pred.get("confidence", 0.0)
79
+ if text:
80
+ extracted_lines.append(text)
81
+ detections.append({"text": text, "confidence": round(conf, 3)})
82
+
83
+ full_text = "\n".join(extracted_lines)
84
+ print(f"[NemotronOCR] Extracted {len(extracted_lines)} lines using {model_name}.")
85
+
86
+ return {
87
+ "status": "SUCCESS",
88
+ "model_used": model_name,
89
+ "extracted_text": full_text if full_text else "No text detected in image.",
90
+ "detections": detections,
91
+ "line_count": len(extracted_lines)
92
+ }
93
+ else:
94
+ print(f"[NemotronOCR] {model_name} returned status {res.status_code}: {res.text}")
95
+ except Exception as e:
96
+ print(f"[NemotronOCR] Exception calling {model_name}: {e}")
97
+
98
+ return {
99
+ "status": "FAILED",
100
+ "model_used": "None",
101
+ "extracted_text": "Failed to extract OCR text via NVIDIA Nemotron API.",
102
+ "detections": [],
103
+ "line_count": 0
104
+ }
rag_engine.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import numpy as np
3
+ from sklearn.feature_extraction.text import TfidfVectorizer
4
+ from sklearn.metrics.pairwise import cosine_similarity
5
+
6
+ class ResumeRAGStore:
7
+ def __init__(self, chunk_size: int = 250, overlap: int = 50):
8
+ self.chunk_size = chunk_size
9
+ self.overlap = overlap
10
+ self.chunks = []
11
+ self.vectorizer = None
12
+ self.tfidf_matrix = None
13
+
14
+ def index_resume_text(self, text: str):
15
+ """
16
+ Chunks raw resume text and indexes it using TF-IDF for RAG retrieval.
17
+ """
18
+ if not text or not text.strip():
19
+ self.chunks = ["No resume content indexed."]
20
+ return
21
+
22
+ # Split into paragraphs and sentences
23
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
24
+ self.chunks = []
25
+
26
+ current_chunk = []
27
+ current_len = 0
28
+
29
+ for line in lines:
30
+ current_chunk.append(line)
31
+ current_len += len(line)
32
+ if current_len >= self.chunk_size:
33
+ self.chunks.append(" ".join(current_chunk))
34
+ current_chunk = current_chunk[-1:] # Keep last line for overlap
35
+ current_len = len(current_chunk[0]) if current_chunk else 0
36
+
37
+ if current_chunk:
38
+ self.chunks.append(" ".join(current_chunk))
39
+
40
+ if not self.chunks:
41
+ self.chunks = [text]
42
+
43
+ # Build TF-IDF index
44
+ self.vectorizer = TfidfVectorizer(stop_words="english")
45
+ try:
46
+ self.tfidf_matrix = self.vectorizer.fit_transform(self.chunks)
47
+ except Exception as e:
48
+ print(f"[RAGStore] TFIDF indexing warning: {e}")
49
+ self.tfidf_matrix = None
50
+
51
+ def retrieve_context(self, query: str, top_k: int = 3) -> str:
52
+ """
53
+ Retrieves top_k most relevant resume passages matching the query.
54
+ """
55
+ if not self.chunks or self.vectorizer is None or self.tfidf_matrix is None:
56
+ return "\n".join(self.chunks[:top_k])
57
+
58
+ try:
59
+ query_vec = self.vectorizer.transform([query])
60
+ scores = cosine_similarity(query_vec, self.tfidf_matrix).flatten()
61
+ top_indices = np.argsort(scores)[::-1][:top_k]
62
+
63
+ relevant_chunks = [self.chunks[i] for i in top_indices if scores[i] > 0.05]
64
+ if not relevant_chunks:
65
+ relevant_chunks = self.chunks[:top_k]
66
+
67
+ return "\n\n---\n\n".join(relevant_chunks)
68
+ except Exception as e:
69
+ print(f"[RAGStore] Context retrieval error: {e}")
70
+ return "\n".join(self.chunks[:top_k])
requirements.txt CHANGED
@@ -1,2 +1,8 @@
1
- gradio>=5.23.0
2
- numpy>=1.26.0
 
 
 
 
 
 
 
1
+ groq>=0.30.0
2
+ requests>=2.28.0
3
+ pillow>=9.0.0
4
+ scikit-learn>=1.0.0
5
+ numpy>=1.20.0
6
+ pandas>=2.0.0
7
+ pyyaml>=6.0
8
+ psutil>=5.9.0
utils.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DEFAULT_SAMPLE_RESUME = """ALEX CHEN
2
+ Senior AI & Machine Learning Engineer | San Francisco, CA | alex.chen@email.com
3
+
4
+ SUMMARY
5
+ Passionate Senior AI Engineer with 6+ years of experience designing and deploying scalable deep learning models, RAG architectures, and computer vision systems. Expertise in PyTorch, Python, TensorFlow, Docker, Kubernetes, and LLM fine-tuning.
6
+
7
+ TECHNICAL SKILLS
8
+ • Languages: Python, C++, SQL, Bash
9
+ • AI & ML Frameworks: PyTorch, TensorFlow, Scikit-learn, OpenCV, Hugging Face
10
+ • LLM & RAG: LangChain, LlamaIndex, Vector DBs (Milvus, Qdrant, FAISS), RAG Optimization
11
+ • MLOps & Cloud: AWS (S3, EC2, SageMaker), Docker, Kubernetes, CI/CD, MLflow
12
+
13
+ PROFESSIONAL EXPERIENCE
14
+ Senior AI Engineer | TechCorp AI (2022 - Present)
15
+ • Led development of enterprise RAG search system improving document retrieval latency by 45%.
16
+ • Fine-tuned 70B parameter LLMs on proprietary datasets using LoRA and QLoRA techniques.
17
+ • Architected automated MLOps pipelines on Kubernetes serving 2M+ daily active requests.
18
+
19
+ EDUCATION
20
+ B.S. in Computer Science | University of California, Berkeley (2015 - 2019)
21
+ """
22
+
23
+ DEFAULT_JOB_DESCRIPTION = """We are seeking a Senior AI/ML Engineer to build next-generation RAG systems, fine-tune open-source Large Language Models (LLMs), and deploy scalable MLOps infrastructure.
24
+
25
+ KEY REQUIREMENTS:
26
+ • 5+ years of software engineering & ML experience in Python and PyTorch.
27
+ • Strong expertise in RAG (Retrieval-Augmented Generation), Vector Databases, and LangChain/LangGraph.
28
+ • Demonstrated experience deploying containerized models on Docker/Kubernetes or Cloud (AWS/GCP).
29
+ • Experience with MLOps tracking tools (MLflow, Weights & Biases) and CI/CD pipelines.
30
+ • B.S. or M.S. in Computer Science or related field.
31
+ """
32
+
33
+ def generate_ats_score_html(score_pct: int) -> str:
34
+ color = "#48bb78" if score_pct >= 80 else ("#ed8936" if score_pct >= 60 else "#f56565")
35
+ return f"""
36
+ <div style='background: linear-gradient(135deg, #1e293b, #0f172a); border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; padding: 20px; text-align: center; box-shadow: 0 8px 32px rgba(0,0,0,0.4);'>
37
+ <div style='font-size: 0.85rem; text-transform: uppercase; color: #94a3b8; font-weight: 700; letter-spacing: 0.05em;'>ATS Match Compatibility Score</div>
38
+ <div style='font-size: 3.2rem; font-weight: 900; color: {color}; margin: 8px 0; font-family: Inter, sans-serif;'>{score_pct}%</div>
39
+ <div style='background: #1a202c; border-radius: 10px; height: 12px; width: 100%; overflow: hidden; border: 1px solid #2d3748;'>
40
+ <div style='background: {color}; height: 100%; width: {score_pct}%; border-radius: 10px; transition: width 0.8s ease-in-out;'></div>
41
+ </div>
42
+ </div>
43
+ """
44
+
45
+ def format_skill_badges(matched_skills: list, missing_skills: list) -> str:
46
+ html = ["<div style='font-family: Inter, sans-serif; padding: 10px;'>"]
47
+ html.append("<h4 style='color: #4ade80; margin-bottom: 8px;'>✅ Matched Qualifications & Skills</h4><div>")
48
+ if matched_skills:
49
+ for s in matched_skills:
50
+ html.append(f"<span style='display: inline-block; background: rgba(34, 197, 94, 0.15); color: #4ade80; border: 1px solid #22c55e; border-radius: 14px; padding: 4px 12px; margin: 4px; font-size: 13px; font-weight: 600;'>✓ {s}</span>")
51
+ else:
52
+ html.append("<span style='color: #94a3b8;'>No explicit matching skills found.</span>")
53
+ html.append("</div><br>")
54
+ html.append("<h4 style='color: #f87171; margin-bottom: 8px;'>⚠️ Missing Key Competencies from Job Description</h4><div>")
55
+ if missing_skills:
56
+ for s in missing_skills:
57
+ html.append(f"<span style='display: inline-block; background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid #ef4444; border-radius: 14px; padding: 4px 12px; margin: 4px; font-size: 13px; font-weight: 600;'>✗ {s}</span>")
58
+ else:
59
+ html.append("<span style='color: #4ade80;'>No major missing skills identified!</span>")
60
+ html.append("</div></div>")
61
+ return "".join(html)