Add Box-Grid-Review step (Step 11) to OCR pipeline
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 44s
CI / test-go-edu-search (push) Successful in 43s
CI / test-python-klausur (push) Failing after 2m52s
CI / test-python-agent-core (push) Successful in 36s
CI / test-nodejs-website (push) Successful in 37s

New pipeline step between Gutter Repair and Ground Truth that processes
embedded boxes (grammar tips, exercises) independently from the main grid.

Backend:
- cv_box_layout.py: classify_box_layout() detects flowing/columnar/
  bullet_list/header_only layout types per box
- build_box_zone_grid(): layout-aware grid building (single-column for
  flowing text, independent columns for tabular content)
- POST /sessions/{id}/build-box-grids endpoint with SmartSpellChecker
- Layout type overridable per box via request body

Frontend:
- StepBoxGridReview.tsx: shows each box with cropped image + editable
  GridTable. Layout type dropdown per box. Auto-builds on first load.
- Auto-skip when no boxes detected on page
- Pipeline steps updated: 13 steps (0-12), Ground Truth moved to 12

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-12 17:26:06 +02:00
parent 52637778b9
commit 5da9a550bf
6 changed files with 661 additions and 2 deletions

View File

@@ -2181,3 +2181,117 @@ async def gutter_repair_apply(session_id: str, request: Request):
)
return result
# ---------------------------------------------------------------------------
# Box-Grid-Review endpoints
# ---------------------------------------------------------------------------
@router.post("/sessions/{session_id}/build-box-grids")
async def build_box_grids(session_id: str, request: Request):
"""Rebuild grid structure for all box zones with layout-aware detection.
For each zone with zone_type='box':
1. Auto-detect layout type (flowing / columnar / bullet_list / header_only)
2. Build grid with layout-appropriate parameters
3. Apply SmartSpellChecker corrections
4. Store results back in grid_editor_result.zones[]
Optional body: { "overrides": { "2": "bullet_list" } }
Maps zone_index → forced layout_type.
"""
session = await get_session_db(session_id)
if not session:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
grid_data = session.get("grid_editor_result")
if not grid_data:
raise HTTPException(status_code=400, detail="No grid data. Run build-grid first.")
word_result = session.get("word_result") or {}
all_words = word_result.get("cells") or word_result.get("words") or []
body = {}
try:
body = await request.json()
except Exception:
pass
layout_overrides = body.get("overrides", {})
from cv_box_layout import classify_box_layout, build_box_zone_grid, _group_into_lines
from grid_editor_helpers import _words_in_zone
img_w = grid_data.get("image_width", 0)
img_h = grid_data.get("image_height", 0)
zones = grid_data.get("zones", [])
box_count = 0
spell_fixes = 0
for z in zones:
if z.get("zone_type") != "box":
continue
bbox = z.get("bbox_px", {})
bx, by = bbox.get("x", 0), bbox.get("y", 0)
bw, bh = bbox.get("w", 0), bbox.get("h", 0)
if bw <= 0 or bh <= 0:
continue
zone_idx = z.get("zone_index", 0)
# Filter words inside this box
zone_words = _words_in_zone(all_words, by, bh, bx, bw)
if not zone_words:
logger.info("Box zone %d: no words found in bbox", zone_idx)
continue
# Get layout override or auto-detect
forced_layout = layout_overrides.get(str(zone_idx))
# Build box grid
box_grid = build_box_zone_grid(
zone_words, bx, by, bw, bh,
zone_idx, img_w, img_h,
layout_type=forced_layout,
)
# Apply SmartSpellChecker to all box cells
try:
from smart_spell import SmartSpellChecker
ssc = SmartSpellChecker()
for cell in box_grid.get("cells", []):
text = cell.get("text", "")
if not text:
continue
result = ssc.correct_text(text, lang="auto")
if result.changed:
cell["text"] = result.corrected
spell_fixes += 1
except ImportError:
pass
# Update zone data with new grid
z["columns"] = box_grid["columns"]
z["rows"] = box_grid["rows"]
z["cells"] = box_grid["cells"]
z["header_rows"] = box_grid.get("header_rows", [])
z["box_layout_type"] = box_grid.get("box_layout_type", "flowing")
z["box_grid_reviewed"] = False
box_count += 1
# Save updated grid back
await update_session_db(session_id, grid_editor_result=grid_data)
logger.info(
"build-box-grids session %s: %d box zones rebuilt, %d spell fixes",
session_id, box_count, spell_fixes,
)
return {
"session_id": session_id,
"box_zones_rebuilt": box_count,
"spell_fixes": spell_fixes,
"zones": zones,
}