Skip to content

Resume Portfolio Builder

A Streamlit resume builder with a live bullet-strength analyzer that scores every line in real time and grades the complete resume out of 100. It coaches users into stronger writing while they type.

Python Streamlit NLP PDF Export Live Scoring
// the problem

Most resume tools just format. They don't teach.

The standard resume tool gives you a template and helps you fill it out. You write your experience bullets, it handles the layout, and you export a PDF. Whether your bullets are any good is entirely up to you. The feedback loop, if there is one, usually comes from a career counselor or a hiring manager rejection, neither of which is timely or specific enough to help you write better.

The weakness of most resume bullets follows a predictable pattern. They use passive or weak verbs: "responsible for," "worked on," "helped with." They describe job duties rather than outcomes. They don't include numbers or metrics where numbers exist. These are well-known problems in the career advice space, but existing tools don't fix them because they don't analyze the text you write.

The idea for this project was to build the analysis into the writing experience itself. You type a bullet point, and the tool immediately tells you how strong it is and why. The score updates on every keystroke. By the time you're done writing, you've already been coached.


// how it works

Per-bullet scoring, whole-resume grade, PDF export

The analyzer evaluates each bullet point across several dimensions: whether it starts with a strong action verb, whether it includes quantifiable results, whether the length is appropriate, whether it contains any weak or filler phrases, and overall specificity. Each dimension has a weight in the final bullet score, which is displayed inline next to the bullet as you type.

The verb check uses a categorized list of action verbs grouped by strength. "Led," "designed," and "reduced" rank higher than "supported," "helped," or "assisted." Bullets starting with passive constructions are flagged regardless of the verb quality.

The whole-resume grade is computed from all bullet scores weighted by section. The experience section matters more than the skills section. The grade updates live as you add or edit content. A resume with consistently strong bullets, proper section structure, and appropriate length will score in the 90s. Most first drafts land in the 50s to 70s, which gives concrete direction.

When you're satisfied with the resume, you can export it as a PDF. The export uses a clean single-column layout that's designed to parse well through applicant tracking systems. The styling is intentionally minimal so the content does the work.


// the technical details

Scoring logic and Streamlit's reactivity model

Streamlit reruns the entire script on every user interaction. For a tool where you want per-keystroke feedback, that means the scoring function runs on every character change. The performance constraint is that the analysis has to be fast enough to feel instant. Pure Python NLP without any heavy model inference is fast enough; the scoring typically completes in a few milliseconds.

The verb detection uses a lookup against a curated word list rather than any learned model. For this specific task, a well-maintained list outperforms embedding-based approaches because the categories are well-defined and the signal you care about is simple: is this word on the strong-verb list or not.

The metric detection is done with a regular expression that matches numbers, percentages, dollar amounts, and multipliers. A bullet that contains a quantified result gets a meaningful score boost. This alone pushes a lot of users toward writing better bullets, because the score feedback makes the improvement visible immediately.

Streamlit doesn't have native PDF generation, so export uses Python's reportlab library to render the resume content into a PDF buffer served as a download. The layout targets ATS parsers: single column, no tables, no text boxes. Getting the typography and line spacing to look right took more iteration in the layout code than expected, but the result passes cleanly through the parsers I tested.