diff --git a/klausur-service/backend/grid_editor_api.py b/klausur-service/backend/grid_editor_api.py index 1b5e1d3..87ce37e 100644 --- a/klausur-service/backend/grid_editor_api.py +++ b/klausur-service/backend/grid_editor_api.py @@ -2189,16 +2189,14 @@ async def gutter_repair_apply(session_id: str, request: Request): @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. + """Rebuild grid structure for all detected boxes 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[] + Uses structure_result.boxes (from Step 7) as the source of box coordinates, + and raw_paddle_words as OCR word source. Creates or updates box zones in + the grid_editor_result. - Optional body: { "overrides": { "2": "bullet_list" } } - Maps zone_index → forced layout_type. + Optional body: { "overrides": { "0": "bullet_list" } } + Maps box_index → forced layout_type. """ session = await get_session_db(session_id) if not session: @@ -2208,8 +2206,20 @@ async def build_box_grids(session_id: str, request: Request): if not grid_data: raise HTTPException(status_code=400, detail="No grid data. Run build-grid first.") + # Get raw OCR words (with top/left/width/height keys) word_result = session.get("word_result") or {} - all_words = word_result.get("cells") or word_result.get("words") or [] + all_words = word_result.get("raw_paddle_words") or word_result.get("raw_tesseract_words") or [] + if not all_words: + raise HTTPException(status_code=400, detail="No raw OCR words available.") + + # Get detected boxes from structure_result + structure_result = session.get("structure_result") or {} + gt = session.get("ground_truth") or {} + if not structure_result: + structure_result = gt.get("structure_result") or {} + detected_boxes = structure_result.get("boxes") or [] + if not detected_boxes: + return {"session_id": session_id, "box_zones_rebuilt": 0, "spell_fixes": 0, "message": "No boxes detected"} body = {} try: @@ -2218,37 +2228,40 @@ async def build_box_grids(session_id: str, request: Request): pass layout_overrides = body.get("overrides", {}) - from cv_box_layout import classify_box_layout, build_box_zone_grid, _group_into_lines + from cv_box_layout import build_box_zone_grid from grid_editor_helpers import _words_in_zone - img_w = grid_data.get("image_width", 0) - img_h = grid_data.get("image_height", 0) + img_w = grid_data.get("image_width", 0) or word_result.get("image_width", 0) + img_h = grid_data.get("image_height", 0) or word_result.get("image_height", 0) zones = grid_data.get("zones", []) + + # Find highest existing zone_index + max_zone_idx = max((z.get("zone_index", 0) for z in zones), default=-1) + + # Remove old box zones (we'll rebuild them) + zones = [z for z in zones if z.get("zone_type") != "box"] + 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) + for box_idx, box in enumerate(detected_boxes): + bx = box.get("x", 0) + by = box.get("y", 0) + bw = box.get("w", 0) + bh = box.get("h", 0) if bw <= 0 or bh <= 0: continue - zone_idx = z.get("zone_index", 0) - - # Filter words inside this box + # Filter raw OCR 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) + logger.info("Box %d: no words found in bbox (%d,%d,%d,%d)", box_idx, bx, by, bw, bh) continue - # Get layout override or auto-detect - forced_layout = layout_overrides.get(str(zone_idx)) + zone_idx = max_zone_idx + 1 + box_idx + forced_layout = layout_overrides.get(str(box_idx)) # Build box grid box_grid = build_box_zone_grid( @@ -2272,26 +2285,46 @@ async def build_box_grids(session_id: str, request: Request): 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 + # Build zone entry + zone_entry = { + "zone_index": zone_idx, + "zone_type": "box", + "bbox_px": {"x": bx, "y": by, "w": bw, "h": bh}, + "bbox_pct": { + "x": round(bx / img_w * 100, 2) if img_w else 0, + "y": round(by / img_h * 100, 2) if img_h else 0, + "w": round(bw / img_w * 100, 2) if img_w else 0, + "h": round(bh / img_h * 100, 2) if img_h else 0, + }, + "border": None, + "word_count": len(zone_words), + "columns": box_grid["columns"], + "rows": box_grid["rows"], + "cells": box_grid["cells"], + "header_rows": box_grid.get("header_rows", []), + "box_layout_type": box_grid.get("box_layout_type", "flowing"), + "box_grid_reviewed": False, + "box_bg_color": box.get("bg_color_name", ""), + "box_bg_hex": box.get("bg_color_hex", ""), + } + zones.append(zone_entry) box_count += 1 - # Save updated grid back + # Sort zones by y-position for correct reading order + zones.sort(key=lambda z: z.get("bbox_px", {}).get("y", 0)) + + grid_data["zones"] = zones 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, + "build-box-grids session %s: %d boxes processed (%d words spell-fixed) from %d detected", + session_id, box_count, spell_fixes, len(detected_boxes), ) return { "session_id": session_id, "box_zones_rebuilt": box_count, + "total_detected_boxes": len(detected_boxes), "spell_fixes": spell_fixes, "zones": zones, }