왜 space-ocr는 다른 LLM OCR과 다른가: 검증 가능한 구조화 추출
LLM에 직접 OCR을 시키는 방식과 space-ocr의 차이. 필드는 페이지 위 좌표와 verified 판정과 함께 돌아오고, 확인할 항목은 review 목록에 실린다.
영수증이나 청구서 한 장을 GPT-4o, Gemini, Claude에 던지면서 합계와 거래처, 품목 내역을 뽑아 달라고 할 수 있다. 대부분은 그럴듯한 JSON이 돌아온다. 문제는 이걸 대량으로 신뢰하려 할 때 시작된다. 모델이 돌려주는 건 문자열이고, 문자열에는 위치가 없다. 합계가 48,200으로 나왔다면 페이지의 어느 픽셀을 읽은 것일까? 그 숫자가 실제로 문서에 인쇄되어 있었을까, 아니면 모델이 그럴듯한 값을 채워 넣은 걸까? LLM 호출만으로는 페이지를 직접 다시 읽어 보기 전까지 이 질문에 답할 수 없다.
바로 이 간극이 범용 LLM을 OCR 도구로 쓰는 것과 space-ocr를 쓰는 것의 차이 전부다. space-ocr는 LLM에 반대하는 물건이 아니다. 내부적으로는 지금 OCR 엔진(Google Cloud Vision)과 구조화를 담당하는 Gemini를 함께 쓰는데, 이건 구현 세부이지 API 계약이 아니다. space-ocr가 더하는 것은 모델을 둘러싼 층이다. 반환하는 값은 OCR이 페이지에서 실제로 본 것과 대조되고, 판정이 붙고, 시트에 업로드하면 조회 가능한 행으로 저장된다. 이 두 가지, 곧 검증할 수 있는 값별 출처와 별도 데이터베이스 없이 조회할 수 있는 구조화된 출력이야말로 LLM 호출이 사용자에게 떠넘기는 부분이다.
LLM 직접 OCR vs space-ocr
| LLM 직접 호출 (GPT-4o / Gemini / Claude) | space-ocr | |
|---|---|---|
| 값별 위치 | JSON 추출 호출로 돌아오는 건 텍스트이고, 별도 OCR 판독과 교차 대조한 출처 좌표는 계약에 들어 있지 않음 | 위치가 잡힌 값마다 박스(0–1000 격자 위의 xmin, ymin, xmax, ymax)와 네 점짜리 방향 사각형. 잡히지 않은 값은 review.flagged에 nobox로 올라옴 |
| 값별 검증 | 없음, 문자열을 그대로 믿어야 함 | 판정은 cells[path].verified, 사유는 review.reasons. 대조 자체는 evidence에 담긴다 — text_match, source, 그리고 match_ratio(값의 문자 중 페이지에서 감지된 심볼에서 찾아낸 비율) |
| 선언한 규칙 | 검증기를 직접 작성 | required·pattern·enum·min/max·near / not_near를 요청과 함께 선언하면 서버에서 판정하고, 위반은 review.flagged에 실림 |
| 값을 맥락에서 확인 | 문서를 직접 다시 읽어야 함 | 앱에서 셀을 클릭하면 원본 이미지의 해당 영역이 그대로 표시됨 |
| 출력 형태 | 프롬프트에 좌우되어 실행마다 달라지는 JSON | 고정 스키마: fields를 한 번 선언하거나 autoFields에 제안을 맡기면 data.values가 그 형태로 돌아옴 |
| 저장과 조회 | 직접 구축해야 함 | POST /ocr/fields는 응답으로 돌려줄 뿐 이미지를 저장하지 않음. 시트에 업로드하면 한 장이 한 행이 되고 GET /view(where, sort, select, limit, offset)로 조회, OCR 재실행 없음, 과금 없음 |
| 문자 체계 | 모델과 프롬프트에 따라 다름 | 일본어, 한국어, 중국어, 영어 등을 자동 감지, 언어 파라미터 없음 |
| 설정 | 직접 만든 프롬프트, 재시도, 파싱, 검증 파이프라인 | Bearer 키로 HTTPS 호출 한 번 |
'검증됨'이 실제로 무슨 뜻인지 짚어 둔다. 과장하기 쉬운 대목이라서다. 언어 모델은 좌표를 만들어 내지 않는다. 각 값의 텍스트와 단어 토큰 힌트를 반환할 뿐이고, 그다음 엔진이 그 텍스트를 Google Cloud Vision이 페이지에서 감지한 심볼과 한 글자씩 대조한다. 박스는 그 실제 심볼 위에 놓이고, 대조 결과는 cells[path].evidence에 실린다. 문자 대조 자체가 text_match, 값의 문자 중 찾아낸 비율이 match_ratio, 박스를 어떻게 잡았는지가 source다. verified는 그 위에 서는 판정이다. 사유가 하나라도 붙으면 false, 대조가 돌고 아무것도 서지 않으면 true, 대조할 상대가 없으면 null이다. 일치가 약하면 low_ratio 같은 사유가 서고 그 path가 review.flagged에 올라온다. 사람에게 넘기는 목록이 바로 이것이다. values는 모델이 읽은 문자열이지 바이트 단위 복사본이 아니라서, 마스터와 정확 일치로 대조해야 할 때는 evidence.printed_text(그 좌표에서 OCR이 읽은 글자)를 쓴다. 이는 모델이 절대 틀리지 않는다는 약속이 아니다. 토큰 힌트는 여전히 어긋날 수 있고, 두 엔진이 같은 오독에 합의할 수도 있다. 각 값을 그냥 믿는 대신 페이지와 대조하고 그 결과를 알려 준다는 뜻이다.
{
"status": "success",
"data": {
"values": {
"vendor": "ACME Trading Co."
},
"cells": {
"vendor": {
"box": { "xmin": 120, "ymin": 84, "xmax": 512, "ymax": 118 },
"quad": [
{ "x": 120, "y": 84 }, { "x": 512, "y": 84 },
{ "x": 512, "y": 118 }, { "x": 120, "y": 118 }
],
"verified": true,
"review": null,
"evidence": {
"text_match": true,
"source": "vision_symbol_match",
"match_ratio": 1.0,
"ocr_confidence": 0.97
}
}
},
"review": {
"unit": "field",
"declared": 1,
"returned": 1,
"boxed": 1,
"verified": 1,
"flagged": []
},
"image": { "width": 1654, "height": 2339 }
}
}박스는 이미지 크기와 무관한 0–1000 격자 위에서 반환되므로, 화면에 그릴 때는 픽셀로 환산한다: pixel_x = xmin / 1000 * image_width. 폭과 높이는 같은 응답의 data.image에서 가져온다 — 이는 실제로 판독한 지면(EXIF 회전 반영, 큰 사진은 축소 완료)이지 보낸 파일이 아니다. 네 점짜리 사각형은 기울거나 회전된 스캔을 따라가며, 좌상단, 우상단, 우하단, 좌하단 순서로 정렬된다. 무언가 어긋나면 같은 path가 data.review.flagged에 { "path": "total", "reasons": ["text_mismatch"] } 형태로 올라온다. 검토자에게 넘기는 것은 임계값을 정해 잘라야 하는 점수가 아니라 그대로 훑을 수 있는 목록이다. 범용 모델에 JSON 추출을 시키는 호출에는 이런 것이 딸려 오지 않으므로, 필요한 감사 추적은 전부 손으로 짜맞춰야 한다.
값에서 조회 가능한 표로
LLM 직접 호출은 JSON에서 끝난다. 그걸 여전히 저장해야 하고, '이번 분기에 40,000을 넘는 청구서는 어느 것인가?'를 묻고 싶어지는 순간 먼저 데이터베이스와 조회 계층부터 구축하게 된다. POST /ocr/fields도 JSON에서 끝난다 — 응답으로 돌려줄 뿐 이미지를 보관하지 않는다. 다른 점은 같은 추출을 저장 계층에 태울 수 있다는 것이다. 컬럼을 정해 시트를 만들고(POST /create) 거기에 페이지를 올리면(POST /upload), 한 장이 같은 values·cells·review를 지닌 한 행이 된다. 그다음부터 조회는 API 호출 한 번이다. GET /view에 where=total>=40000, sort=-invoice_date, select=vendor,total, 그리고 페이징용 limit과 offset을 붙이면 된다. 서버 측에서 실행되고, OCR을 다시 돌리지 않으며, 과금되지 않는다. 시트는 CSV로 내보낼 수 있다(BOM이 붙은 UTF-8이라 일본어, 한국어, 중국어 텍스트와 통화가 Excel에서 제대로 열리고, 품목 배열은 각자의 행으로 펼쳐진다).
LLM 직접 호출이 더 나은 경우
범용 LLM은 한 번만 읽으면 되거나, 느슨한 요약이 필요하거나, 문서가 무엇을 뜻하는지 추론해야 할 때 맞는 선택이다. '이 계약서는 무슨 내용인가?'는 모델에게 물을 질문이지, 좌표까지 붙은 OCR에 물을 질문이 아니다. 문서를 대량으로 처리하면서 각 값이 검증 가능하고, 일관되게 구조화되고, 조회 가능해야 한다면 space-ocr를 쓰면 된다. 매입 채무 자동화, 경비 정산, 명함을 CRM에 넣기, 쌓인 영수증 디지털화 같은 일이다. 정직하게 말하면 space-ocr는 검증과 저장 계층을 얹은 LLM 기반 OCR이지, 모델 자체와 경쟁하는 물건이 아니다.
과금은 스캔당 종량제이며, 선택형 월정액 플랜과 매월 제공되는 무료 스캔이 있고, 실패한 스캔은 과금하지 않는다. 현재 요금은 요금 페이지에서 확인할 수 있다.