https://txtfetch.com/docs/quality-checks/
Quality checks for extracted text
Extraction returns success. That does not tell you the text is right, and you cannot read 200,000 documents to find out by hand.
The concession first
A 200 response is not a quality signal. None of the checks below prove a document's text is correct. They find the documents that are probably wrong instead. That is a different, cheaper problem, and it is the one you can solve at scale.
Layer 1 — automatic checks, no ground truth needed
These run on the extracted text alone. No human, and no reference copy of the document, is needed. Each one is a real check in scanExtractionQuality(), with the threshold it fires at.
| Check | Fires at | What it catches |
|---|---|---|
| Hyphenated line breaks | 1 or more | A print-layout hyphen and line break split one word in two, like inter-\nnational. |
| Mojibake | 1 or more | Text decoded with the wrong character encoding, such as UTF-8 read as Windows-1252. |
| Replacement characters | 1 or more | A byte the decoder could not map to any character, and replaced with �. |
| Unexpanded ligatures | 1 or more | A single glyph, like fi, standing in for a letter pair a tokenizer expects as two. |
| Missing spaces between words | 5 or more | Two words fused with no space, from a PDF that never encoded one between the glyph runs. |
| Repeated header or footer lines | 3 or more repeats of the same line | A running header, footer, or stamp, extracted as ordinary body text on every page. |
| Bare page-number lines | 2 or more | A line that is only a page number, like "3" or "Page 3 of 42". |
| Hard-wrapped lines | 3 or more | A paragraph kept as one line per print-width line, instead of reflowed into one block. |
| Collapsed long lines | 1 or more lines over 300 characters, with no sentence punctuation | Likely two interleaved PDF columns, read as one long line in the wrong order. |
| Space-run table columns | 2 or more lines | A table flattened into space-separated text, with no row or column markers left. |
| No paragraph breaks | over 2,000 characters, with no blank line anywhere | A document long enough to need paragraph structure, with none of it left to key off. |
Two more checks need the file itself, not just the returned text, so they sit outside scanExtractionQuality().
| Check | What it catches |
|---|---|
| Empty or near-empty result | A document that returned almost no text at all. This is the check for a scanned PDF that no OCR pass read, or a scanned page that carried a typed footer or stamp and lost its scan. Read /docs/images for what comes back from pictures and scans. |
| Characters per page, below your own floor | Compare characters extracted against page count, per document. Set your own floor from your corpus, not a number this page invents. A page far under your usual average is worth a look, even when the whole document is not empty. |
Every finding here links to the fix page for that symptom: why it happens, and how to repair it.
Layer 2 — sampling
Layer 1 does not catch everything. A wrong number, a mistranslated word, or a table read in the right shape but the wrong values all pass every automatic check. Someone still has to read a sample.
- Read every document a layer-1 check flagged. That is not optional sampling; it is the cheapest signal you have.
- Stratify the rest by format. A PDF pipeline and a DOCX pipeline fail in different ways, so one clean sample of PDFs says nothing about your DOCX documents.
- Stratify by source, too. Documents from one scanner, one customer, or one upstream system tend to share the same defect.
- Treat the sample as a practical check, not a statistical guarantee. A clean sample of 20 documents does not prove the other 199,980 are clean.
Layer 3 — a golden set and a regression run
Keep 20 to 50 documents with human-checked expected text: the correct words, the correct table cells, the correct reading order. Re-run them on every pipeline change, before it ships, not after.
Score each run with the same metrics /benchmarks already publishes, so you are not inventing a new definition of "correct":
- Text similarity
- Table cell-F1
- Table structure ("TEDS-style")
- Reading order
- Structure validity
See /benchmarks for what each metric means and how it is scored. See /diff for the worked example: raw extraction next to the corrected text, on real documents.
A parser that silently drops content is the defect this page exists to catch. The reverse risk is real too: text a document's author expected to stay hidden, coming back intact. See personal data in extracted text for where that happens and what to do about it. Hidden instructions are the same risk: untrusted documents covers them.
What to do with a flagged document
A runnable layer-1 check
A small Python function with five of the layer-1 checks from the table above, at the same thresholds.
import re
from collections import Counter
def quality_report(text):
"""Layer-1 checks, same thresholds as the published table."""
findings = {}
hyphen_breaks = len(re.findall(r"[A-Za-z]-\n[a-z]", text))
if hyphen_breaks >= 1:
findings["hyphenated-linebreaks"] = hyphen_breaks
missing_spaces = len(re.findall(r"[a-z][A-Z]", text)) + len(re.findall(r"\.[A-Za-z]", text))
if missing_spaces >= 5:
findings["missing-spaces"] = missing_spaces
lines = [l.strip() for l in text.split("\n") if 3 <= len(l.strip()) <= 80]
repeats = max(Counter(lines).values(), default=0)
if repeats >= 3:
findings["repeated-lines"] = repeats
long_lines = sum(1 for l in text.split("\n") if len(l) > 300 and not re.search(r"[.!?]", l))
if long_lines >= 1:
findings["collapsed-long-lines"] = long_lines
if len(text) > 2000 and "\n\n" not in text:
findings["no-paragraph-breaks"] = 1
return findings
Frequently asked questions
- Does a 200 response mean the extracted text is correct?
- No. It means the request succeeded, not that the text is right. A scanned page with no OCR, a scrambled table, or mojibake can all return a clean 200. The checks on this page find the documents that are probably wrong, which is a cheaper problem than proving every document is right.
- Can these checks prove my extracted text is accurate?
- No. They catch known damage patterns: mojibake, fused words, scrambled columns, and the rest of the table above. A document can pass every check and still contain a wrong number or a mistranslated word. Only a golden set with human-checked expected text measures accuracy. Even that measures it on the documents in the set, not on every document you process.
- How many documents should a golden set hold?
- 20 to 50 is enough to catch a regression without becoming a maintenance burden. Pick documents that cover your real formats and your hardest cases, not a random sample. Re-run the set on every pipeline change, and treat a score drop like a failing test.
- What do I do with a document a check flags?
- Read the matching /fixes page first; it explains the likely cause. Then re-run the document at a higher quality tier, or route it to a review queue instead of your index. See "what to do with a flagged document" above.
Watch it run, then take a key.
The playground replays a real recorded response for every sample document.
Open the playground →