The repository is at github.com/sebastianspicker/AI-PDF-Renamer.
Repository configuration and Ollama documentation checked through 2026-07-11. Model names, context settings, endpoint compatibility, and remote-service privacy are configuration-dependent and should be rechecked against the installed release before use.
The Problem
Every PDF acquisition pipeline eventually produces the same chaos.
Journal articles downloaded from publisher sites arrive as
513194-008.pdf or 1-s2.0-S0360131520302700-main.pdf. Scanned
letters from the tax authority arrive as scan0023.pdf. Invoices arrive
as Rechnung.pdf — every invoice from every vendor, overwriting each
other if you are not paying attention. The actual content is
in the file. The filename tells you nothing.
The human solution is trivial: open the PDF, glance at the title or date or sender, type a descriptive name. Thirty seconds per file, multiplied by several hundred files accumulated over a year, becomes a task that perpetually does not get done.
The automated solution sounds equally trivial: read the text, decide what the document is, generate a filename. What could be involved?
Quite a bit, it turns out. Working through the implementation is a useful way to make concrete some things about LLMs and text processing that are easy to understand in the abstract but clearer with a specific task in front of you.
Step One: Getting Text Out of a PDF
A PDF is not a text file. It is a binary format designed for page layout and print fidelity — it encodes character positions, fonts, and rendering instructions, not a linear stream of prose. The text in a PDF has to be extracted by a parser that reassembles it from the position data.
For PDFs with embedded text (most modern documents), this works well enough. For scanned PDFs — images of pages, with no embedded text at all — you need OCR as a fallback. The pipeline handles both: native extraction first, OCR if the text yield is below a useful threshold.
The result is a string. Already there are failure modes: two-column layouts produce interleaved text if the parser reads left-to-right across both columns simultaneously; footnotes appear in the middle of sentences; tables produce gibberish unless the parser handles them specifically. These are not catastrophic — for renaming purposes, the first paragraph and the document header are usually enough, and those are less likely to be badly formatted than the body. But they are real, and they mean that the text passed to the next stage is not always clean.
Step Two: The Token Budget
Once you have a string representing the document’s text, you cannot simply pass all of it to a language model. Two reasons: context windows have hard limits, and — even when they are large enough — filling them with the full text of a thirty-page document is wasteful for a task that only needs the title, date, and category.
Language models do not process characters directly. They process tokens — units produced by a model-specific tokenizer, often a subword tokenizer but not necessarily the same BPE scheme I described in the strawberry post. A rough practical rule for English text is:
$$N_{\text{tokens}} \;\approx\; \frac{N_{\text{chars}}}{4}$$This is an approximation — technical text, non-English content, and
code tokenise differently — but it is useful for budgeting. A ten-page
academic paper might contain around 30,000 characters, which is
approximately 7,500 tokens under a suitable tokenizer. The repository’s apple-silicon preset
uses qwen2.5:3b via Ollama and documents a 32K context target; the gpu preset documents
qwen2.5:7b-instruct and 128K. Actual usable context depends on the model, runtime, and configured
limits. The prompt and response also consume context.
The tool defaults to 28,000 tokens of extracted text
(DEFAULT_MAX_CONTENT_TOKENS), leaving comfortable headroom for the
prompt and response in most configurations. For documents that exceed this, the extraction
is truncated — typically to the first N characters, on the reasonable
assumption that titles, dates, and document types appear early.
This truncation is a design decision, not a limitation to be apologised for. For the renaming task, the first two pages of a document contain everything the filename needs. A strategy that extracts the first page plus the last page (which often has a date, a signature, or a reference number) would work for some document types. The current implementation keeps it simple: take the front, stay within budget.
Step Three: Heuristics First
Here is something that improves almost any LLM pipeline for structured extraction tasks: do as much work as possible with deterministic rules before touching the model.
The AI PDF Renamer applies a scoring pass over the extracted text before deciding whether to call the LLM at all. The heuristics are regex-based rules that look for patterns likely to appear in specific document types:
- Date patterns:
\d{4}-\d{2}-\d{2},\d{2}\.\d{2}\.\d{4}, and a dozen variants - Document type markers: “Rechnung”, “Invoice”, “Beleg”, “Gutschrift”, “Receipt”
- Author/institution lines near the document header
- Keywords from a configurable list associated with specific categories
Each rule that fires contributes a score to a candidate metadata record. If the heuristic pass produces a confident result — date found, category identified, a couple of distinguishing keywords present — the LLM call is skipped entirely. The file gets renamed from the heuristic output.
This matters because heuristics can be deterministic and avoid a model call. Their absolute speed and the share of documents they handle depend on hardware, rules and corpus; no benchmark for the former microseconds-versus-seconds or “most invoices” claims was supplied here.
The LLM is intended for cases not resolved confidently by rules: unusual formats, mixed languages or ambiguous document types. The repository review did not establish a representative 20–40% fallback rate.
Step Four: What to Ask the LLM, and How
When a heuristic pass does not produce a confident result, the pipeline builds a prompt from the extracted text and sends it to the local endpoint. What the prompt asks for matters enormously.
The naive approach: “Please rename this PDF. Here is the content: [text].” The response will be a sentence. Maybe several sentences. It will not be parseable as a filename without further processing, and that further processing is itself an LLM call or a fragile regex.
The better approach: ask for structured output. The prompt in
llm_prompts.py requests a JSON object conforming to a schema — something
like:
{
"date": "YYYYMMDD or null",
"category": "one of: invoice, paper, letter, contract, ...",
"keywords": ["max 3 short keywords"],
"summary": "max 5 words"
}
The model returns JSON. The response parser in llm_parsing.py validates
it against the schema, catches malformed responses, applies fallbacks for
null fields, and sanitises the individual fields before they are assembled
into a filename.
Structured JSON creates a schema that can be parsed and validated. Reliability still depends on model, runtime, decoding, prompt and document; no cross-model failure rate or training-data comparison was measured for this article.
Schema checks can reject shape and allowed-value errors. They do not by themselves prove that a plausible date or keyword is supported by the source; that requires source-grounded validation and human review in the dry run.
Step Five: The Filename
The output format is YYYYMMDD-category-keywords-summary.pdf. A few
design decisions embedded in this:
Date first. Lexicographic sorting of filenames then gives you chronological sorting for free. This is the most useful sort order for most document types — you want to find the most recent invoice, not the alphabetically first one.
Lowercase, hyphens only. No spaces (which require escaping in many
contexts), no special characters (which are illegal in some filesystems
or require quoting), no uppercase (which creates case-sensitivity issues
across platforms). The sanitisation step in filename.py strips or
replaces anything that does not conform.
Collision resolution. Two documents with the same date, category,
keywords, and summary would produce the same filename. The resolver
appends a counter suffix (_01, _02, …) when a target name already
exists. Order-independent naming would additionally require a stable sort and
tie-break rule; that property was not execution-tested in this review.
Local-First
The repository’s Apple-Silicon and GPU presets use Ollama at http://127.0.0.1:11434; its
documented configured completion URL is http://127.0.0.1:11434/v1/completions.
Ollama’s current OpenAI-compatibility documentation prominently shows /v1/chat/completions,
so endpoint compatibility is a release/configuration check rather than a universal API fact. This
local default is a deliberate choice
for a document management tool. The documents being renamed are likely
to include medical records, financial statements, legal correspondence —
content that should not be routed through an external API by default.
The default preset is a 3B Qwen 2.5 model, while the GPU preset is a 7B instruction model; model choice, latency, and extraction quality depend on hardware, runtime, language, scan quality, and the documents. This is a utility task that can often use a small local model, but suitability should be established with a representative dry run rather than inferred from parameter count.
For users who want to use a remote API, the endpoint is configurable. That changes the data-flow and privacy boundary; the local default is a sensible starting point, not a guarantee once a remote endpoint, proxy, hook, or third-party OCR service is configured.
What It Cannot Do
Renaming is a classification problem disguised as a text generation problem. The tool works well when documents have standard structure — title on page one, date near the header or footer, document type identifiable from a few keywords. It works less well for documents that are structurally atypical: a hand-written letter scanned at poor resolution, a PDF that is essentially a single large image, a document in a language the model handles badly.
The heuristic fallback means that even when the LLM produces a bad result, the file gets a usable if imperfect name rather than a broken one. And the undo log means that a bad batch run can be reversed. These are not complete solutions to the hard cases, but they are the right design response to a tool that handles real-world document noise.
The harder limit is semantic: the tool can tell you that a document is an invoice and extract its date and vendor name. It cannot tell you whether the invoice has been paid, whether it matches a purchase order, or whether the amount is correct. For those questions, renaming is just the first step in a longer pipeline.
The repository is at github.com/sebastianspicker/AI-PDF-Renamer. The tokenisation background in the extraction and budgeting sections connects to the strawberry tokenisation post and the context window post.
Changelog
- 2026-04-02: Corrected the default model name from
qwen3:8btoqwen2.5:3b. The codebase default isqwen2.5:3b(apple-silicon preset) orqwen2.5:7b-instruct(gpu preset). - 2026-04-02: Corrected
DEFAULT_MAX_CONTENT_TOKENSdescription from “28,000 characters … roughly 7,000 tokens” to “28,000 tokens.” The variable is a token limit, not a character limit. - 2026-07-11: Corrected tokenizer, model, endpoint, privacy, benchmark, JSON-validation, fallback-rate, and collision-order claims to distinguish documented defaults from untested behaviour.