feat: OCR pipeline v2.1 – narrow column OCR, dewarp automation, Fabric.js editor
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 24s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Failing after 1m50s
CI / test-python-agent-core (push) Successful in 19s
CI / test-nodejs-website (push) Successful in 15s
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 24s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Failing after 1m50s
CI / test-python-agent-core (push) Successful in 19s
CI / test-nodejs-website (push) Successful in 15s
Proposal B: Adaptive padding, crop upscaling, PSM selection, row-strip re-OCR for narrow columns (<15% width) – expected accuracy boost 60-70% → 85-90%. Proposal A: New text-line straightness detector (Method D), quality gate (rejects counterproductive corrections), 2-pass projection refinement, higher confidence thresholds – expected manual dewarp reduction to <10%. Proposal C: Fabric.js canvas editor with drag/drop, inline editing, undo/redo, opacity slider, zoom, PDF/DOCX export endpoints. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1742,6 +1742,151 @@ async def save_reconstruction(session_id: str, request: Request):
|
||||
}
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/reconstruction/fabric-json")
|
||||
async def get_fabric_json(session_id: str):
|
||||
"""Return cell grid as Fabric.js-compatible JSON for the canvas editor."""
|
||||
session = await get_session_db(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
|
||||
|
||||
word_result = session.get("word_result")
|
||||
if not word_result:
|
||||
raise HTTPException(status_code=400, detail="No word result found")
|
||||
|
||||
cells = word_result.get("cells", [])
|
||||
img_w = word_result.get("image_width", 800)
|
||||
img_h = word_result.get("image_height", 600)
|
||||
|
||||
from services.layout_reconstruction_service import cells_to_fabric_json
|
||||
fabric_json = cells_to_fabric_json(cells, img_w, img_h)
|
||||
|
||||
return fabric_json
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/reconstruction/export/pdf")
|
||||
async def export_reconstruction_pdf(session_id: str):
|
||||
"""Export the reconstructed cell grid as a PDF table."""
|
||||
session = await get_session_db(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
|
||||
|
||||
word_result = session.get("word_result")
|
||||
if not word_result:
|
||||
raise HTTPException(status_code=400, detail="No word result found")
|
||||
|
||||
cells = word_result.get("cells", [])
|
||||
columns_used = word_result.get("columns_used", [])
|
||||
grid_shape = word_result.get("grid_shape", {})
|
||||
n_rows = grid_shape.get("rows", 0)
|
||||
n_cols = grid_shape.get("cols", 0)
|
||||
|
||||
# Build table data: rows × columns
|
||||
table_data: list[list[str]] = []
|
||||
header = [c.get("label", c.get("type", f"Col {i}")) for i, c in enumerate(columns_used)]
|
||||
if not header:
|
||||
header = [f"Col {i}" for i in range(n_cols)]
|
||||
table_data.append(header)
|
||||
|
||||
for r in range(n_rows):
|
||||
row_texts = []
|
||||
for ci in range(n_cols):
|
||||
cell_id = f"R{r:02d}_C{ci}"
|
||||
cell = next((c for c in cells if c.get("cell_id") == cell_id), None)
|
||||
row_texts.append(cell.get("text", "") if cell else "")
|
||||
table_data.append(row_texts)
|
||||
|
||||
# Generate PDF with reportlab
|
||||
try:
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle
|
||||
import io as _io
|
||||
|
||||
buf = _io.BytesIO()
|
||||
doc = SimpleDocTemplate(buf, pagesize=A4)
|
||||
if not table_data or not table_data[0]:
|
||||
raise HTTPException(status_code=400, detail="No data to export")
|
||||
|
||||
t = Table(table_data)
|
||||
t.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#0d9488')),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 9),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('WORDWRAP', (0, 0), (-1, -1), True),
|
||||
]))
|
||||
doc.build([t])
|
||||
buf.seek(0)
|
||||
|
||||
from fastapi.responses import StreamingResponse
|
||||
return StreamingResponse(
|
||||
buf,
|
||||
media_type="application/pdf",
|
||||
headers={"Content-Disposition": f'attachment; filename="reconstruction_{session_id}.pdf"'},
|
||||
)
|
||||
except ImportError:
|
||||
raise HTTPException(status_code=501, detail="reportlab not installed")
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/reconstruction/export/docx")
|
||||
async def export_reconstruction_docx(session_id: str):
|
||||
"""Export the reconstructed cell grid as a DOCX table."""
|
||||
session = await get_session_db(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
|
||||
|
||||
word_result = session.get("word_result")
|
||||
if not word_result:
|
||||
raise HTTPException(status_code=400, detail="No word result found")
|
||||
|
||||
cells = word_result.get("cells", [])
|
||||
columns_used = word_result.get("columns_used", [])
|
||||
grid_shape = word_result.get("grid_shape", {})
|
||||
n_rows = grid_shape.get("rows", 0)
|
||||
n_cols = grid_shape.get("cols", 0)
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt
|
||||
import io as _io
|
||||
|
||||
doc = Document()
|
||||
doc.add_heading(f'Rekonstruktion – Session {session_id[:8]}', level=1)
|
||||
|
||||
# Build header
|
||||
header = [c.get("label", c.get("type", f"Col {i}")) for i, c in enumerate(columns_used)]
|
||||
if not header:
|
||||
header = [f"Col {i}" for i in range(n_cols)]
|
||||
|
||||
table = doc.add_table(rows=1 + n_rows, cols=max(n_cols, 1))
|
||||
table.style = 'Table Grid'
|
||||
|
||||
# Header row
|
||||
for ci, h in enumerate(header):
|
||||
table.rows[0].cells[ci].text = h
|
||||
|
||||
# Data rows
|
||||
for r in range(n_rows):
|
||||
for ci in range(n_cols):
|
||||
cell_id = f"R{r:02d}_C{ci}"
|
||||
cell = next((c for c in cells if c.get("cell_id") == cell_id), None)
|
||||
table.rows[r + 1].cells[ci].text = cell.get("text", "") if cell else ""
|
||||
|
||||
buf = _io.BytesIO()
|
||||
doc.save(buf)
|
||||
buf.seek(0)
|
||||
|
||||
from fastapi.responses import StreamingResponse
|
||||
return StreamingResponse(
|
||||
buf,
|
||||
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
headers={"Content-Disposition": f'attachment; filename="reconstruction_{session_id}.docx"'},
|
||||
)
|
||||
except ImportError:
|
||||
raise HTTPException(status_code=501, detail="python-docx not installed")
|
||||
|
||||
|
||||
@router.post("/sessions/{session_id}/reprocess")
|
||||
async def reprocess_session(session_id: str, request: Request):
|
||||
"""Re-run pipeline from a specific step, clearing downstream data.
|
||||
|
||||
Reference in New Issue
Block a user