A role-aware resume ranking web app built with Flask, spaCy NLP, and Vanilla JS. ResumRank AI was developed as part of the Ramadan Hackathon 2026 by CIS Community NEDUET. It showcases local NLP processing, deterministic scoring algorithms, multi-stage pipeline design, and transparent result generation in a full-stack context.
The project demonstrates practical NLP and web engineering applied to a real hiring problem: parsing PDF resumes, extracting skills and experience using local language models, scoring candidates against a job description, and ranking them with full explainability. Features emphasize how backend processing maps to real-world HR entities (resumes, job requirements, skill gaps, experience levels) with clear pipeline stages and no reliance on external AI APIs.
- PDF Parsing: Multi-page text and table extraction using pdfplumber; detects and warns on scanned/image-only PDFs (< 100 characters extracted).
- Local NLP Extraction: spaCy
en_core_web_smfor named entity recognition; regex date-range parsing for experience calculation; 500+ tech skill keyword database with alias resolution. - Deterministic Scoring: Transparent formula —
Final Score = (Skill Match × 70%) + (Experience Score × 30%); every score is reproducible, auditable, and explainable. - Intelligent Ranking: Multi-level tie-breaking — final score → skill score → experience score → alphabetical name; identical triple scores receive the same rank number.
- Gap Analysis: Template-based human-readable explanations per candidate across four tiers — Strong Match (80+), Moderate Match (60–79), Weak Match (40–59), and Not Recommended (< 40).
- Results Dashboard: Sortable rankings table, color-coded score badges, skill pills (matched/missing), expandable per-candidate gap analysis rows, and summary statistics bar.
- CSV Export: Timestamped download with candidate names, scores, matched/missing skills, gap analysis text, and experience years.
- Duplicate Detection: MD5 hash check on uploaded files; blocks identical resumes in the same batch regardless of filename.
- Rate Limiting: 5 analyses per minute per IP enforced in-memory with rolling time windows.
- Auto Cleanup: Uploaded files deleted immediately after processing; sessions and results expire after 1 hour.
- No API Keys Required: Entire pipeline runs locally — no Gemini, no OpenAI, no external services, no cost.
| Component | Purpose |
|---|---|
| Python / Flask | Web framework, routing, pipeline orchestration |
| spaCy en_core_web_sm | Local NLP — named entity recognition and skill extraction |
| pdfplumber | PDF text and table extraction |
| Pandas / NumPy | CSV generation and data manipulation |
| Gunicorn | Production WSGI server |
| Custom CSS (dark theme) | Professional UI, responsive layout, no framework dependencies |
| Vanilla JS | Drag-drop upload, real-time progress polling, results table sorting |
ResumRank/
├── app.py # Flask app, routes, pipeline orchestrator, session management
├── config.py # Constants, scoring weights, environment settings
├── .env.example # Environment variable template
├── .gitignore # Ignore rules for temp/test artifacts
├── requirements.txt # Python dependencies
├── runtime.txt # Python version pin (3.11)
├── Procfile # Heroku deployment command
├── test_nlp.py # Pipeline verification test suite (5 tests)
│
├── modules/
│ ├── __init__.py # Package imports + spaCy setup verification
│ ├── skills_db.py # 500+ tech skills database with aliases and normalization
│ ├── skill_extractor.py # spaCy NLP + regex extraction pipeline
│ ├── scorer.py # Weighted scoring engine + gap analysis templates
│ ├── ranker.py # Deterministic ranking and tie-breaking logic
│ ├── pdf_parser.py # PDF text extraction with edge case handling
│ ├── session_manager.py # File-based session persistence
│ └── exporter.py # CSV generation and formatting
│
├── templates/
│ ├── index.html # Upload page with drag-drop and real-time progress bar
│ ├── results.html # Rankings dashboard with sorting and CSV export
│ └── error.html # Error page template
│
├── static/
│ ├── css/style.css # Dark professional theme
│ ├── js/main.js # Drag-drop, progress polling, column sorting
│ └── favicon.svg # Bar chart logo icon
│
└── README.md
- Clone the repo and navigate into the project folder.
- Install dependencies:
pip install -r requirements.txt
- Download the spaCy language model (one-time, ~150MB):
python -m spacy download en_core_web_sm
- (Optional) Create a
.envfile for custom settings:APP_ENV=development SECRET_KEY=your-dev-secret PORT=5000 - Run the development server:
python app.py
- Open http://localhost:5000 in your browser.
Environment variables (optional locally, required in production):
SECRET_KEY— Flask session secret; generate withpython -c "import secrets; print(secrets.token_hex(32))"APP_ENV— Set toproductionon Heroku; defaults todevelopmentPORT— Set automatically by Heroku; defaults to5000
If APP_ENV=production and SECRET_KEY is missing or left at the default dev value, the app now fails fast at startup.
- Upload Resumes — Drag and drop up to 10 PDF files onto the upload zone (or click Browse). Files are validated for type, size (max 10MB), and duplicates before saving.
- Paste Job Description — Enter the full job posting including required skills, experience, and responsibilities (minimum 20 words for reliable extraction).
- Analyze — Click Analyze and watch real-time progress as PDFs are parsed, skills extracted, scored, and ranked.
- Review Results — View the ranked table showing final scores, skill match percentages, experience scores, years, matched skills, and missing skills per candidate.
- Expand Gap Analysis — Click any candidate row to see a full written explanation of their strengths, gaps, and hire recommendation.
- Export — Download a timestamped CSV for use in your ATS or hiring workflow.
| Concept | How It Appears in ResumRank AI |
|---|---|
| Named Entity Recognition | spaCy PERSON entities extract candidate names from resume headers |
| Regex-based Date Parsing | Date range patterns calculate years of experience from work history |
| Keyword Matching | Word-boundary regex matches 500+ skills against resume and job text |
| Alias Resolution | Skill aliases normalize variations (React.js → react, k8s → kubernetes) |
| Education Context Filtering | Proximity heuristic excludes education-section dates from experience calculation |
| Fallback Chains | Name extraction tries NER → regex → cleaned filename as progressive fallbacks |
| Weighted Scoring | Two-component formula with configurable weights (70/30 split) |
| Deterministic Ranking | Multi-key sort guarantees identical input always produces identical output |
- Skill extraction accuracy: Word-boundary matching (
\bpython\b) prevents false positives — "r" won't match "react", "go" won't match "good". All known aliases for a skill are checked independently and deduplicated before scoring, so "React.js" and "React" in the same resume count once. - Experience calculation: All date ranges in a resume are parsed across four formats (full month name, abbreviated month, MM/YYYY, year-only). Education-context ranges are filtered by checking for proximity to keywords (university, bachelor, degree, gpa). The max-span approach (earliest start date → latest end date) avoids double-counting overlapping jobs.
- Gap analysis tiers: Candidates scoring 80+ receive "Strong Match" with immediate interview recommendation; 60–79 "Moderate Match" with skills-assessment suggestion; 40–59 "Weak Match" with training caveat; below 40 "Not Recommended" with specific missing areas listed.
- Deterministic ranking: The multi-key sort
(-final_score, -skill_score, -experience_score, name)guarantees the same input always produces the same ranking, making results auditable and reproducible. - Tied ranks: Candidates sharing identical (final_score, skill_score, experience_score) receive the same rank number and subsequent ranks are skipped (e.g., 1, 1, 3), not compressed.
- Session isolation: Each analysis gets a UUID; results, progress, and uploaded files are keyed to that UUID and cleaned up independently without affecting concurrent sessions.
- Input validation before processing: File type, file size, job description word count, and MD5 duplicate hashes are all checked before any file is saved or pipeline invoked.
- Graceful degradation: If spaCy NER fails to extract a name, the extractor falls back to regex, then to a cleaned version of the filename — the pipeline never crashes on a missing field.
- Batch resilience: If skill extraction fails for one candidate in a batch, that candidate receives zero scores and a fallback entry; other candidates still score and rank correctly.
- Safe cleanup: File deletion and session removal use patterns that don't raise if a key or file is already gone — idempotent by design.
- Scanned PDF handling: PDFs yielding fewer than 100 characters are flagged as likely scanned images with a visible warning badge; they receive a fallback empty extraction rather than crashing.
| Scenario | Typical Time |
|---|---|
| Parse 1 PDF | 0.5–2 sec |
| Extract skills (1 resume) | 0.3–1 sec |
| Score + rank 5 resumes | < 1 sec |
| Full pipeline (5 resumes) | 5–10 sec total |
No network latency. No rate limits. No API quotas.
- Extend the skills database (
skills_db.py) with domain-specific skills and observe how scoring shifts for resumes in that domain. - Adjust the scoring weights in
config.py(SKILL_WEIGHT,EXPERIENCE_WEIGHT) and re-run the same batch to compare ranking outcomes. - Add a new experience tier (e.g., 8+ years → 110) and observe how it affects candidates with long careers.
- Test the education-context filter by adding a resume where work dates and graduation dates overlap — confirm the experience calculation excludes the degree years.
- Run
test_nlp.pyafter modifying an alias inskills_db.pyto see the validation catch broken canonical mappings.
python test_nlp.pyRuns 5 verification tests covering the full pipeline:
- spaCy Installation — model loads without error and version is confirmed
- Skills Database — all 500+ skills present, aliases resolve to valid canonicals,
validate_skills_database()passes - Job Skill Parsing — expected skills extracted from a sample job description
- Resume Extraction — name, email, experience years, skills found, relevant/missing skills all parse correctly from a sample resume
- Gap Analysis — all four scoring tiers produce correctly labelled, appropriately worded output
All 5 tests pass after model download. Expected output:
Results: 5/5 tests passed
✅ All tests passed — ready to deploy!
This repository includes Heroku-ready configuration:
Procfiledefines the process type and startup commandruntime.txtpins Python to version 3.11- Session and upload storage use
/tmp(ephemeral but writable)
Steps:
- Push this repo to GitHub.
- Create a new Heroku app:
heroku create my-resumrank-app - Set environment variables:
heroku config:set APP_ENV=production heroku config:set SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(32))") - Deploy:
git push heroku main - Open:
heroku open
Notes:
- The app uses local spaCy NLP and installs
en_core_web_smat build time (first deploy ~30 seconds). - Session data and uploaded files are stored in
/tmp(ephemeral; clears on dyno restart). - For persistent storage, add a database or object storage addon (not required for demo/MVP).
python app.py
# Runs on http://localhost:5000Abdul Hayy Khan
abdulhayykhan.1@gmail.com
This project is open-source and available for educational and commercial use under the MIT License.
