space ocr
가이드아티클요금문서
developer

스캔에서, 사람이 읽는 순서 그대로의 텍스트 꺼내기

素의 OCR 은 페이지의 모든 단어를 검출 순서로 돌려줍니다. 그건 누구도 읽지 않는 순서입니다. 읽기순서를 바로잡은 원문 텍스트 추출, 블록별 좌표, 그리고 글자가 바뀌지 않았는지에 대한 검증 이야기.

6 분 분량· 2026-07-28

"그냥 텍스트만 주세요" — 가장 쉬워 보이는 요청이지만, 많은 OCR 연동이 조용히 어긋나는 지점입니다.

비전 OCR 엔진은 찾아낸 단어를 문단으로 묶어 검출 순서로 돌려줍니다. 페이지 전체를 대략 위에서 아래로, 왼쪽에서 오른쪽으로. 단 한 단으로 된 메모라면 우연히 읽는 순서와 맞습니다. 2단 기사, 사이드바가 있는 청구서, 살짝 기울어져 스캔된 페이지에서는 맞지 않습니다. 왼쪽 단의 첫 문단, 다음에 오른쪽 단의 첫 문단, 다시 왼쪽으로. 단어는 전부 맞습니다. 문서로는 말이 안 됩니다.

그 텍스트가 검색 색인이나 임베딩, 비교로 들어간다면 피해는 조용합니다. 에러는 나지 않고, 결과만 서서히 나빠지며, 아무도 OCR 단계까지 거슬러 올라가지 않습니다.

구조가 없어도 원문 텍스트에 필요한 세 가지

지킬 구조가 없더라도, 쓸 만한 텍스트 추출은 세 가지 질문에 답해야 합니다.

  • 어떤 순서인가 — 블록이 읽기순서로 나와야 하고, 줄바꿈으로 끊긴 문장은 두 조각으로 남지 않고 다시 붙어야 합니다.
  • 어디서 왔는가 — 문단을 색인한 뒤 "왜 이 문서가 걸렸는지"를 사람에게 보여 주려면 그 문단의 좌표가 필요합니다. 페이지 번호로는 부족합니다.
  • 페이지에 적힌 그대로인가 — 페이지를 옮겨 적는 언어 모델은 동시에 "다듬어" 놓기도 합니다. 조용히 이뤄진 개선은 오류보다 고약합니다. 정확하게 읽히니까요.

호출 한 번

POST /ocr/text 는 이미지를 받아 그 문서의 텍스트를 돌려줍니다. includeBlocks 를 요청하면 각 블록이 좌표와 함께 옵니다.

1
2
3
4
5
6
7
8
curl -X POST https://api.space-ocr.com/ocr/text \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "https://example.com/page.jpg",
    "imageType": "url",
    "includeBlocks": true
  }'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "status": "success",
  "data": {
    "text": "사쿠라상사 주식회사\n청구서\n합계 1,451",
    "source": "llm",
    "image_size": { "width": 1654, "height": 2339 },
    "review_summary": {
      "unit": "block",
      "total": 12,
      "boxed": 12,
      "text_verified": 11,
      "text_mismatch": 1,
      "needs_review": 1,
      "flagged": [{ "path": "blocks[7]", "reason": "text_mismatch" }],
      "recovered_blocks": 0,
      "token_coverage": 1.0
    },
    "blocks": [
      {
        "text": "사쿠라상사 주식회사",
        "bbox": { "xmin": 60, "ymin": 48, "xmax": 470, "ymax": 92 },
        "vertices": [{ "x": 60, "y": 48 }, "…"],
        "bbox_source": "token_id",
        "text_verified": true,
        "needs_review": false
      }
    ]
  }
}

text 는 문서 전체를 한 문자열로 이은 것(블록을 개행으로 연결)입니다. blocks[] 는 같은 내용을 실제로 위치가 특정된 단위로 나눈 것이고, 각 블록에 0~1000 정규화 bboxvertices 가 붙습니다.

역할 분담은 명시해 둘 가치가 있습니다. 글자와 좌표의 진실은 비전 OCR 이 갖고, 언어 모델에게는 순서와 묶음만 묻습니다. 어떤 블록이 다음인지, 어떤 조각이 한 덩어리인지는 모델이 정하지만, 좌표를 만들어 낼 수는 없고 글자에 대해서도 최종 결정권을 갖지 않습니다.

text_verified, 그리고 그것이 false 일 때

블록마다 text_verified 가 붙습니다. 엔진은 그 블록이 주장하는 단어 토큰을 가져와, 그 좌표에서 비전 OCR 이 읽은 내용을 조회하고, 정규화한 뒤 대조합니다. true 는 두 판독이 일치했다는 뜻이고, false 는 옮겨 적은 것이 페이지와 다르다는 뜻으로, 텍스트는 그대로 주되 조용히 틀리는 대신 표시됩니다.

이것은 정확도 퍼센트가 아닙니다. 더 좁고 더 쓸모 있는 질문에 답합니다 — 이건 픽셀과 대조했고, 일치했는가? 색인 파이프라인이라면 전부 색인하되 플래그는 보존해서, 최종적으로 사람에게 매칭을 보여 줄 때 불확실한 블록을 사실처럼 내놓지 않는 운용이 자연스럽습니다.

✓ Verified

두 가지 실패 방식은 숨기지 않고 명시적으로 다룹니다. 모델 패스가 완전히 실패하면(키 없음·쿼터 소진·재시도 소진) 엔드포인트는 에러를 내지 않고 비전 전사로 강등되며, source: "vision"warning 으로 그렇다고 알립니다. 또 어떤 블록도 청구하지 않은 페이지 토큰 구간은 회수 블록(bbox_source: "unclaimed_tokens")으로 말미에 되돌아와, 흘린 문단이 출력에서 사라지는 대신 출력에 나타납니다.

모델을 꺼야 할 때

useLlm: false 는 비전 전용 전사를 돌려줍니다. 즉시 응답·모델 비용 0, 대신 블록은 raw OCR 순서입니다. 순서가 자명하게 맞는 단단 조판 페이지나, 키워드 존재 여부만 보면 되는 일괄 처리에서는 이쪽이 옳은 거래입니다.

순서가 의미를 실어 나르는 곳에서는 켜 두세요 — 다단 조판, 사이드바가 있는 양식, 기울어진 스캔, 그리고 기계 매칭만이 아니라 사람에게 보여 줄 텍스트.

그리고 산문이 아니라 구조가 필요하다면(제목은 제목으로, 표는 표로) 다른 엔드포인트입니다. POST /ocr/markdown 이 요소별로 같은 좌표와 같은 text_verified 를 가진 레이아웃 보존 마크다운을 돌려줍니다.

관련 글