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
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:
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user