Add _clean_cell_text() with three sub-filters to remove OCR noise:
- _is_garbage_text(): vowel/consonant ratio check for phantom row garbage
- _is_noise_tail_token(): dictionary-based trailing noise detection
- _RE_REAL_WORD check for cells with no real words (just fragments)
Handles balanced parentheses "(auf)" and trailing hyphens "under-"
as legitimate tokens while stripping noise like "Es)", "3", "ee", "B".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two generic noise filters added to _ocr_single_cell():
1. Word confidence filter (conf < 30): removes low-confidence words
before text assembly. Catches trailing artifacts like "Es)" after
real text, and standalone noise from image edges.
2. Cell noise filter: clears cells whose entire text has no real
alphabetic word (>= 2 letters). Catches fragments like "E:", "3",
"u", "D", "2.77", "and )" from image areas, while keeping real
short words like "Ei", "go", "an".
Both filters apply to word-lookup AND cell-OCR fallback results.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The cell grid IS the result. Each cell stays at its detected position.
Removed _split_comma_entries and _attach_example_sentences from the
pipeline — they were shuffling content between rows/columns, causing
"Mäuse" to appear in a separate row, "stand..." to move to Example,
and "Ei" to disappear.
Now: cells → _cells_to_vocab_entries (1:1 row mapping) →
_fix_character_confusion → _fix_phonetic_brackets → done.
Also lowered pixel-density threshold from 2% to 0.5% for the cell-OCR
fallback so small text like "Ei" is not filtered out.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three non-generic solutions replaced with universal heuristics:
1. Cell-OCR fallback: instead of restricting to column_en/column_de,
now checks pixel density (>2% dark pixels) for ANY column type.
Truly empty cells are skipped without running Tesseract.
2. Example-sentence detection: instead of checking for example-column
text (worksheet-specific), now uses sentence heuristics (>=4 words
or ends with sentence punctuation). Short EN text without DE is
kept as a vocab entry (OCR may have missed the translation).
3. Comma-split: re-enabled with singular/plural detection. Pairs like
"mouse, mice" / "Maus, Mäuse" are kept together. Verb forms like
"break, broke, broken" are still split into individual entries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs in the post-processing pipeline were overwriting correct
streaming results with wrong ones:
1. _split_comma_entries was splitting "Maus, Mäuse" into two separate
entries. Disabled — word forms belong together.
2. _attach_example_sentences treated "Ei" (2 chars) as OCR noise due
to `len(de) > 2` threshold. Lowered to `len(de) > 1`.
3. _attach_example_sentences wrongly classified rows with EN text but
no DE (like "stand ...") as example sentences, merging them into
the previous entry. Now only treats rows as examples if they also
have no text in the example column.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip Tesseract fallback for column_example cells which are often
legitimately empty. This reduces ~48 Tesseract calls to ~10,
cutting Step 5 fallback time from ~13s to ~3s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Word-lookup from full-page Tesseract is fast but can miss small or
isolated words (e.g. "Ei"). Now falls back to per-cell Tesseract OCR
for cells that remain empty after word-lookup. The ocr_engine field
reports 'cell_ocr_fallback' for cells that needed the fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace per-cell word filtering (which allowed the same word to appear in
multiple columns due to padded overlap) with exclusive nearest-center
assignment. Each word is assigned to exactly one column per row.
Also use row height as Y-tolerance for text assembly so words within
the same row (e.g. "Maus, Mäuse") are always grouped on one line.
Fixes: words leaking into wrong columns, missing words, duplicate words.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The row_result stored in DB excludes words to keep payload small.
When Step 5 reconstructs RowGeometry from DB, words were empty,
causing word-lookup to find nothing and return blank cells.
Now re-populates row.words from cached _word_dicts (or re-runs
detect_column_geometry if cache is cold) before cell grid building.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace per-cell Tesseract re-runs with lookup of pre-existing full-page
words from row.words. Words are filtered by X-overlap with column bounds.
This fixes phantom rows with garbage text, missing last words, and
incomplete example text by using the more reliable full-page OCR results.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rows in inter-line whitespace gaps have no Tesseract words assigned but
were still processed by build_cell_grid, producing garbage OCR output.
Filter these phantom rows using the word_count field set during Step 4.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cells now appear one-by-one in the UI as they are OCR'd, with a live
progress bar, instead of waiting for the full result.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract build_cell_grid() as layout-agnostic foundation from
build_word_grid(). Step 5 now produces a generic cell grid (columns x
rows) and auto-detects whether vocab layout is present. Frontend
dynamically switches between vocab table (EN/DE/Example) and generic
cell table based on layout type.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The validation that rejected word-center grid when it produced more rows
than gap-based detection was causing fallback to gap-based rows (large
boxes). The word-center grid regularization works correctly after the
center-based grouping and cluster merging fixes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes:
1. Grid validation: reject word-center grid if it produces MORE rows
than gap-based detection (more rows = lines were split = worse).
Falls back to gap-based rows in that case.
2. Words overlay: draw clean grid cells (column × row intersections)
instead of padded entry bboxes. Eliminates confusing double lines.
OCR text labels are placed inside the grid cells directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The y_tolerance for word-center clustering was based on median word
height (21px → 12px tolerance), which was too small. Words on the
same line can have centers 15-20px apart due to different heights.
Now uses 40% of the gap-based median row height as tolerance (e.g.
40px row → 16px tolerance), and 30% for merge threshold. This
produces correct cluster counts matching actual text lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When columns change (Step 3), invalidate row_result and word_result.
When rows change (Step 4), invalidate word_result.
This ensures Step 5 always uses the latest row boundaries instead of
showing stale cached word_result from a previous run.
Applies to both auto-detection and manual override endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix half-height rows caused by tall special characters (brackets, IPA
symbols) being split into separate line clusters:
- Group words by vertical CENTER instead of TOP position, so tall
characters on the same line stay in one cluster
- Filter outlier-height words (>2× median) when computing letter_h
so brackets/IPA don't skew the row height
- Merge clusters closer than 0.4× median word height (definitely
same text line despite slight center differences)
- Increased y_tolerance from 0.5× to 0.6× median word height
- Enhanced logging with cluster merge count and row height range
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace rigid uniform grid with bottom-up approach that derives row
boundaries from word vertical centers:
- Group words into line clusters, compute center_y per cluster
- Compute pitch (distance between consecutive centers)
- Detect section breaks where gap > 1.8× median pitch
- Place row boundaries at midpoints between consecutive centers
- Per-section local pitch adapts to heading/paragraph spacing
- Validate ≥85% word placement, fallback to gap-based rows
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace _split_oversized_rows() with _regularize_row_grid(). When ≥60%
of content rows have consistent height (±25% of median), overlay a
uniform grid with the standard row height over the entire content area.
This leverages the fact that books/vocab lists use constant row heights.
Validates grid by checking ≥85% of words land in a grid row. Falls back
to gap-based rows if heights are too irregular or words don't fit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement _split_oversized_rows() in detect_row_geometry() (Step 7) to
split content rows >1.5× median height using local horizontal projection.
This produces correctly-sized rows before word OCR runs, instead of
working around the issue in Step 5 with sub-cell splitting hacks.
Removed Step 5 workarounds: _split_oversized_entries(), sub-cell
splitting in build_word_grid(), and median_row_h calculation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
For cells taller than 1.5× median row height, split vertically into
sub-cells and OCR each separately. This fixes RapidOCR losing text
at the bottom of tall cells (e.g. "floor/Fußboden" below "egg/Ei"
in a merged row). Generic fix — works for any oversized cell.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Integrate Britfone dictionary (MIT, 15k British English IPA entries)
- Add pronunciation parameter: 'british' (default) or 'american'
- British uses Britfone (Received Pronunciation), falls back to CMU
- American uses eng_to_ipa/CMU, falls back to Britfone
- Frontend: dropdown to switch pronunciation, default = British
- API: ?pronunciation=british|american query parameter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Semantic example matching: instead of attaching example sentences
to the immediately preceding entry, find the vocab entry whose
English word(s) appear in the example. "a broken arm" → matches
"broken" via word overlap, not "egg/Ei". Uses stem matching for
word form variants (break/broken share stem "bro").
2. Cell padding: add 8px padding to each cell region so words at
column/row edges don't get clipped by OCR (fixes "er wollte"
missing at cell boundaries).
3. Treat very short DE text (≤2 chars) as OCR noise, not real
translation — prevents false positives in example detection.
All fixes are generic and deterministic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 4 post-processing steps after OCR (no LLM needed):
1. Character confusion fix: I/1/l/| correction using cross-language
context (if DE has "Ich", EN "1" → "I")
2. IPA dictionary replacement: detect [phonetics] brackets, look up
correct IPA from eng_to_ipa (MIT, 134k words) — replaces OCR'd
phonetic symbols with dictionary-correct transcription
3. Comma-split: "break, broke, broken" / "brechen, brach, gebrochen"
→ 3 individual entries when part counts match
4. Example sentence attachment: rows with EN but no DE translation
get attached as examples to the preceding vocab entry
All fixes are deterministic and generic — no hardcoded word lists.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Preserve \n between visual lines within cells (instead of joining with space)
- Rejoin hyphenated words split across line breaks (e.g. Fuß-\nboden → Fußboden)
- Split oversized rows (>1.5× median height) into sub-entries when EN/DE
line counts match — deterministic fix for missed Step 4 row boundaries
- Frontend: render \n as <br/>, use textarea for multiline editing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Switch to PP-OCRv5 Latin model (supports ä, ö, ü, ß)
- Use SERVER model for better accuracy
- Lower Det.unclip_ratio 1.6→1.3 to reduce word merging
- Raise Det.box_thresh 0.5→0.6 for stricter detection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix A: Use _group_words_into_lines() with adaptive Y-tolerance to
correctly order words in multi-line cells (fixes word reordering bug).
RapidOCR: Add as alternative OCR engine (PaddleOCR models on ONNX
Runtime, native ARM64). Engine selectable via dropdown in UI or
?engine= query param. Auto mode prefers RapidOCR when available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PSM 7 (single line) missed the second line in cells with two lines.
PSM 6 handles multi-line content. Also fix sort order to Y-then-X
for correct reading order.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend: build_word_grid() intersects column regions with content rows,
OCRs each cell with language-specific Tesseract, and returns vocabulary
entries with percent-based bounding boxes. New endpoints: POST /words,
GET /image/words-overlay, ground-truth save/retrieve for words.
Frontend: StepWordRecognition with overview + step-through labeling modes,
goToStep callback for row correction feedback loop.
MkDocs: OCR Pipeline documentation added.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Build a word-coverage mask so only pixels near Tesseract word bounding
boxes contribute to the horizontal projection. Image regions (high ink
but no words) are treated as white, preventing illustrations from
merging multiple vocabulary rows into one.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Step 4 (row detection) between column detection and word recognition.
Uses horizontal projection profiles + whitespace gaps (same method as columns).
Includes header/footer classification via gap-size heuristics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Column detection now uses vertical projection profiles to find whitespace
gaps between columns, then validates gaps against word bounding boxes to
prevent splitting through words. Old clustering algorithm extracted as
fallback (_detect_columns_by_clustering) for pages with < 2 detected gaps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Reduce left-side threshold from 35% to 20% of content width
- Strong language signal (eng/deu > 0.3) now prevents page_ref assignment
- Increase column_ignore word threshold from 3 to 8 for edge columns
- Apply language guard to Level 1 and Level 2 classification
Fixes: column with deu=0.921 was misclassified as page_ref because
reference score check ran before language analysis.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address 5 weaknesses found via ground-truth comparison on session df3548d1:
- Add column_ignore for edge columns with < 3 words (margin detection)
- Absorb tiny clusters (< 5% width) into neighbors post-merge
- Restrict page_ref to left 35% of content area across all 3 levels
- Loosen marker thresholds (width < 6%, words <= 15) and add strong
marker score for very narrow non-edge columns (< 4%)
- Add EN/DE position tiebreaker when language signals are both weak
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Side-by-side view: auto result (readonly) vs GT editor where teacher
draws correct columns. Diff table shows Auto vs GT with IoU matching.
GT data persisted per session for algorithm tuning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sub-alignments within a column (indented words, etc.) were 60-90px apart
and not getting merged at 3%. On a typical 5-col page (~1500px), 6% = ~90px
merges sub-alignments while keeping real column boundaries (~300px) separate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clusters now track Y-positions of their words and filter by vertical
coverage (>=30% primary, >=15%+5words secondary) to reject noise from
indentations or page numbers. Merge distance widened to 3% content width.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ersetzt hardcodierte Positionsregeln durch ein zweistufiges System:
Phase A erkennt Spaltengeometrie (Clustering), Phase B klassifiziert
Typen per Inhalt (Sprache/Rolle) mit 3-stufiger Fallback-Kette.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace projection-profile layout analysis with Tesseract word bounding
box clustering to detect 5-column vocabulary layouts (page_ref, EN, DE,
markers, examples). Falls back to projection profiles when < 3 clusters.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sessions werden jetzt in PostgreSQL gespeichert statt in-memory.
Neue Session-Liste mit Name, Datum, Schritt. Sessions ueberleben
Browser-Refresh und Container-Neustart. Step 3 nutzt analyze_layout()
fuer automatische Spaltenerkennung mit farbigem Overlay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The old displacement-map approach shifted entire rows by a parabolic
profile, creating a circle/barrel distortion. The actual problem is
a linear vertical shear: after deskew aligns horizontal lines, the
vertical column edges are still tilted by ~0.5°.
New approach:
- Detect shear angle from strongest vertical edge slope (not curvature)
- Apply cv2.warpAffine shear to straighten vertical features
- Manual slider: -2.0° to +2.0° in 0.05° steps
- Slider initializes to auto-detected shear angle
- Ground truth question: "Spalten vertikal ausgerichtet?"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The old -3.0 to +3.0 scale multiplied the full displacement map (up to ~79px)
directly, causing extreme distortion at values >1. New slider:
- 0% = no correction
- 100% = auto-detected correction (default)
- 200% = double correction
- Step size: 5%
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix dewarp method selection: prefer methods with >5px curvature over
higher confidence (vertical_edge 79px was being ignored for text_baseline 2px)
- Add grid overlay on left image in Dewarp step for side-by-side comparison
- Add GET /sessions/{id} endpoint to reload session data
- StepDeskew accepts sessionId prop to restore state when navigating back
- SessionInfo type extended with optional deskew_result and dewarp_result
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>