This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
What I Built
Resume Scorer ā a local-first, open-source resume screening pipeline that turns a PDF into a structured, evidence-backed score in under two minutes.
Drop in a resume PDF, point it at a role, and get:
- Category scores with evidence ā open source contributions, self projects, production experience, technical skills, AI fluency
- Bonus points ā GSoC, portfolio site, startup founder history, LinkedIn presence
- Deductions ā no project links, tutorial-only projects, unverifiable claims
- Key strengths and areas for improvement in plain English
Who would pay for this: any small company or startup drowning in junior applications. A 10-person startup posting a junior engineering role on LinkedIn can easily get 300ā500 applicants in a week. Nobody has time to read all of those carefully. Resume Scorer runs the first pass locally, ranks the pile, and surfaces the top candidates for a human to actually read.
It also works for freelance recruiters who handle screening for multiple clients ā they can define a custom role config per client, run it on their own machine, and deliver faster, more consistent results.
The scoring rubric lives entirely in a role.json file. Adding a new role requires zero Python ā just run python score.py --init-role <name> and edit the scaffolded config.
Code
Resume Scorer
A local-first, open-source resume screening tool. Drop in a PDF, point it at a role, get a structured score with evidence ā no cloud account required.
How it works
- Extract ā PyMuPDF converts the PDF to Markdown text
- Parse ā an LLM reads each resume section (basics, work, education, skills, projects, awards) and returns structured JSON
- Enrich ā if the resume has a GitHub link, the tool fetches repository data and classifies projects
- Score ā the LLM evaluates the enriched resume against the role's rubric and returns category scores, bonus points, deductions, and evidence
- Output ā a human-readable report is printed to stdout; in dev mode a CSV row is appended for batch analysis
The scoring rubric lives entirely in roles/<role_name>/role.json. No Python edits are needed to add, modify, or remove a role.
Why open source models
Running locally on Ollama means:
- No data leaves your machine.ā¦
How I Built It
The entire pipeline runs on open-weight models via Ollama or any OpenAI-compatible endpoint. No closed API is required.
Open-source stack
| Layer | What it does |
|---|---|
| Ollama + Gemma | Local inference. gemma4:latest is the default and runs on a laptop with 16 GB RAM |
| PyMuPDF / pymupdf4llm | Converts PDF pages to clean Markdown before any LLM call |
| Jinja2 | All prompts are templates rendered from role.json at runtime ā no hardcoded rubric text anywhere in the codebase |
| Pydantic | The LLM response is validated against a dynamically-built schema derived from the role config. Invalid point values are rejected before they reach the output |
| GitHub API | Enriches the resume with real contributor data, classifying repos as open_source (multiple contributors) vs self_project (single contributor) |
Architecture
score.py is the CLI entrypoint. It calls:
-
core/pdf.pyā extracts a structuredJSONResumeobject section-by-section, each section getting its own LLM call with a Jinja template and a Pydantic schema for structured output -
core/github.pyā fetches and classifies repositories if the resume has a GitHub link -
core/evaluator.pyā assembles everything into a single text block and calls the LLM with the role's evaluation prompt, fully generated fromrole.json
The scoring schema is built dynamically with pydantic.create_model at runtime, so it reflects whatever categories the role defines. Adding a new role is a JSON edit ā no Python required.
Project layout
resume-scorer/
āāā score.py # CLI entrypoint
āāā roles.py # Role dataclass, load_role(), scaffold_role()
āāā core/ # All processing logic
ā āāā models.py # Pydantic schemas + dynamic scoring models
ā āāā evaluator.py # LLM evaluation pipeline
ā āāā pdf.py # PDF ā structured JSON extraction
ā āāā github.py # GitHub enrichment
ā āāā transform.py # JSON normalisation + text serialisation
ā āāā llm_utils.py # Provider init + JSON extraction helpers
āāā app_config/ # Configuration
ā āāā config.py # Provider resolution
ā āāā providers.json # LLM provider registry
āāā prompts/ # Jinja templates
ā āāā templates/
āāā roles/ # One subdirectory per role
ā āāā software_engineering_intern/
ā āāā role.json # Rubric lives here
ā āāā criteria.jinja # Data-driven ā loops over role.json
ā āāā system_message.jinja
āāā resumes/
Why Does Open Innovation Matter?
Three concrete reasons this couldn't work the same way with a closed API:
1. Candidate data stays on your machine.
Resumes contain names, phone numbers, addresses, education history, and employment records. Many candidates have no idea their PDF might be forwarded to a third-party model provider. Running locally means none of that data leaves the recruiter's machine. This matters legally (GDPR Article 22 has explicit requirements around automated decision-making) and practically ā a small company can't afford a data breach from a screening tool.
2. The unit economics only work locally.
Screening 500 applicants with a paid API at typical pricing costs $20ā60 per batch in tokens alone, before GitHub enrichment calls. At volume, that's a real line item that makes the service unviable for small teams. On Ollama with a local model, the marginal cost per run is zero. The only cost is the hardware you already own.
3. The rubric is yours to inspect and own.
A closed screening API is a black box. You can't see why a candidate scored 62 vs 71, can't tune the weights, can't add a bonus for a skill that matters to your specific team. Because both the model and the prompt are open and local, every scoring decision is fully auditable ā the evidence field in the output tells you exactly what the model saw and why it scored what it scored. When a candidate disputes a result, you can answer them with specifics.
Open-weight models made the privacy and cost arguments viable. The open-source tooling around them ā Ollama's API compatibility, Pydantic's schema enforcement, Jinja's prompt templating ā made the config-driven rubric design possible. A closed API can't give you either.
Prize Categories
Gemma ā Best Use of Gemma
gemma4:latest is the default model shipped in app_config/providers.json. The entire PDF extraction pipeline (six separate structured-output LLM calls per resume) and the final evaluation call all run against Gemma by default through Ollama. Switching to a different model is a one-line change in .env ā no code changes required.
GitHub ā Best Use of GitHub Copilot
This project was built using Kiro, an AI-powered IDE built on VS Code with GitHub Copilot at its core. The agentic coding workflow ā design, implement, verify in a loop ā drove the entire build: the config-driven rubric refactor (making all scoring data-driven from role.json), the folder restructure into core/ and app_config/ packages, import rewrites across the whole codebase, and automated verification runs after every change.
Top comments (0)