근거를 한 줄로 제한했더니 정답이 탈락했습니다: OCR excerpt를 네 줄까지 허용한 이유

evidence는 짧을수록 좋아 보입니다.

영수증에서 값을 읽었다면 그 값이 있는 한 줄만 인용하면 될 것 같습니다.

저도 처음에는 그렇게 만들었습니다.

excerpt는 OCR page의 정확히 한 line 안에 있어야 했습니다.

규칙은 단순했고, 멀리 떨어진 label과 amount를 이어 붙이는 hallucination도 막았습니다.

그러다 실제 한국 영수증 capture가 들어왔습니다.

한 품목이 이런 모양으로 인식됐습니다.

해태)홈런볼피스타치오카
8801019320293
1
2,000

첫 줄은 상품명입니다.

둘째 줄은 barcode입니다.

셋째 줄은 수량입니다.

넷째 줄은 금액입니다.

모델이 네 줄을 그대로 인용하고 올바른 품목을 읽어도 한 줄 규칙에서는 실패합니다.

guard가 hallucination을 막은 것이 아닙니다.

영수증이 실제로 인쇄되고 OCR이 실제로 나눈 구조를 금지한 것입니다.

안전한 규칙이 실제 입력을 읽지 못하면 안전하지 않습니다

한 줄 제한에는 분명한 장점이 있습니다.

다음 같은 조합을 허용하지 않습니다.

아메리카노 1개
합계
소계 4500
부가세 450

모델이 첫 줄의 상품명과 마지막 줄의 세금을 붙여 evidence라고 부르면 안 됩니다.

아메리카노 1개
부가세 450

하지만 “모든 multi-line excerpt를 거절한다”는 규칙은 너무 큽니다.

떨어진 줄의 짜깁기와 인접한 줄에 인쇄된 한 품목을 구분하지 못합니다.

두 경우 모두 newline이 들어 있다는 이유만으로 같은 실패가 됩니다.

실제 capture가 보여준 것은 guard를 없애라는 요구가 아니었습니다.

guard가 보호해야 할 property를 다시 정의하라는 요구였습니다.

필요한 것은 one-line evidence가 아니라 짧고 인접한 line run이었습니다.

허용 범위를 ‘아무 줄이나’가 아니라 ‘인접한 네 줄’로 넓혔습니다

현재 MAX_EVIDENCE_LINES4입니다.

excerpt는 한 줄부터 네 줄까지 사용할 수 있습니다.

단, line은 page에서 연속되어야 하고 원래 순서를 유지해야 합니다.

그래서 다음은 통과합니다.

해태)홈런볼피스타치오카
8801019320293
1
2,000

다음은 통과하지 않습니다.

7-ELEVEN
해태)홈런볼피스타치오카
8801019320293
1
2,000

다섯 줄이기 때문입니다.

중간 line을 건너뛴 excerpt도 통과하지 않습니다.

합계
부가세 450

page에서 두 줄 사이에 소계 4500이 있다면 contiguous run이 아닙니다.

규칙을 넓혔지만 허용한 것은 “여러 줄”이 아니라 “관측된 길이 안의 인접한 여러 줄”입니다.

숫자 4는 이론이 아니라 현재 capture가 요구한 경계입니다

처음 multi-line evidence를 허용한 commit은 실제 capture가 요구한 세 줄을 다뤘습니다.

다음 capture에서는 quantity가 독립된 line으로 분리되면서 네 줄이 필요해졌습니다.

세 줄 cap에서는 다시 올바른 reading이 거절됐습니다.

그래서 cap을 네 줄로 올렸습니다.

이 순서가 중요합니다.

먼저 “넉넉하게 10줄쯤” 정한 것이 아닙니다.

한 줄에서 시작했고, 실제 capture가 실패를 증명했을 때 세 줄로, 다시 다른 capture가 증명했을 때 네 줄로 바뀌었습니다.

4는 모든 영수증에 대한 보편 법칙이 아닙니다.

현재 공개 구현이 관측한 최소 경계입니다.

다섯 줄이 필요한 실제 capture가 나타날 수 있습니다.

그때는 fixture와 실패를 함께 추가하며 올려야 합니다.

그 전까지 cap을 키우는 것은 안전 여유가 아니라 검증 면적 확대입니다.

cap이 없으면 page 전체가 evidence가 됩니다

왜 인접성만 보고 길이는 제한하지 않을까요?

영수증 전체 line도 모두 인접해 있기 때문입니다.

모델이 page 전체를 excerpt로 인용하면 그 안에는 merchant, date, item, total, reference가 모두 들어갈 수 있습니다.

그러면 어떤 claim도 “evidence 안에 값이 있다”는 검사를 통과하기 쉬워집니다.

excerpt가 evidence를 좁혀주는 기능을 잃습니다.

테스트는 이 경계를 직접 고정합니다.

verifyEvidence(fourLineExcerpt, page) === true;
verifyEvidence(fiveLineExcerpt, page) === false;

네 줄 cap은 멀리 떨어진 줄의 splice뿐 아니라 “page를 통째로 인용하기”도 막습니다.

빈 문자열이 모든 문자열에 포함되는 문제와 모양은 다르지만 결과는 비슷합니다.

너무 넓은 excerpt는 아무 값에나 붙일 수 있습니다.

evidence는 존재하기만 해서는 안 되고 claim의 위치를 충분히 좁혀야 합니다.

검증과 화면 box가 같은 findLineRuns()를 사용합니다

text evidence가 통과해도 화면에서 엉뚱한 곳에 box를 그리면 문제가 끝나지 않습니다.

사용자는 “이 값은 여기에서 읽었습니다”라는 시각적 설명을 보게 됩니다.

guard와 anchoring이 서로 다른 normalization이나 line-match 규칙을 쓰면 다음 같은 drift가 생길 수 있습니다.

  • API는 verified라고 반환하지만 scanner image에서는 box를 찾지 못한다.
  • API는 한 excerpt라고 보지만 UI는 다른 line을 가리킨다.
  • whitespace나 full-width 문자를 한쪽만 정규화해 결과가 갈린다.

현재 구현은 verifyEvidence()anchorToLines()가 모두 findLineRuns()를 호출합니다.

findLineRuns()가 하는 일은 다음과 같습니다.

  1. excerpt와 각 OCR line을 같은 방식으로 정규화한다.
  2. 각 시작 line에서 길이 1부터 4까지의 연속 run을 만든다.
  3. excerpt가 들어 있는 가장 짧은 run을 기록한다.
  4. page 안에서 가능한 모든 run을 반환한다.

정규화는 NFKC를 적용하고 horizontal whitespace를 하나로 줄입니다.

newline은 보존합니다.

line boundary 자체가 evidence 의미의 일부이기 때문입니다.

공통 함수가 있으니 “같은 excerpt가 맞는가?”라는 기초 질문은 한 곳에서 정의됩니다.

같은 후보를 찾되 ambiguity를 처리하는 방식은 다릅니다

공통 findLineRuns()를 쓴다고 두 caller의 반환 규칙까지 같지는 않습니다.

verifyEvidence()는 match가 하나 이상 있으면 true입니다.

같은 amount가 영수증에 두 번 찍혀 있어도 excerpt 자체가 page에 존재한다는 사실은 맞습니다.

반면 anchorToLines()는 match가 정확히 하나일 때만 box를 반환합니다.

예를 들어 6.03이 total과 card line에 모두 있으면 첫 번째를 임의로 선택하지 않습니다.

SUBTOTAL
6.03
6.03

값은 evidence text로 검증될 수 있지만 어느 pixel을 가리켜야 하는지는 결정할 수 없습니다.

이때 box는 null입니다.

“box가 없음”이 “틀린 box”보다 낫다는 판단입니다.

공통 match 정의와 caller별 ambiguity policy를 분리하니 둘을 같은 것으로 오해하지 않으면서 drift도 막을 수 있었습니다.

여러 line의 box는 enclosing rectangle 하나가 됩니다

multi-line excerpt를 허용하면 한 line frame만 돌려줄 수 없습니다.

anchorToLines()는 run에 포함된 모든 OCR frame의 최소 enclosing rectangle을 계산합니다.

세 line frame이 다음처럼 흩어져 있다고 해보겠습니다.

name       x=20, y=100, width=300, height=20
barcode    x=10, y=130, width=200, height=20
amount     x=240, y=130, width=80, height=24

반환되는 box는 가장 왼쪽 x=10부터 가장 오른쪽 x=320까지, 가장 위 y=100부터 가장 아래 y=154까지 감쌉니다.

{
  "x": 10,
  "y": 100,
  "width": 310,
  "height": 54
}

사용자는 item name 일부가 아니라 모델이 인용한 전체 evidence 영역을 보게 됩니다.

single-line excerpt는 기존 frame을 그대로 반환합니다.

multi-line 지원 때문에 단순한 case의 geometry가 달라지지 않습니다.

모든 multi-line 문제를 cap 확대로 풀지는 않았습니다

OCR은 영수증 표를 column 단위로 평평하게 만들기도 합니다.

예를 들어 item name 두 개가 먼저 나오고, amount 두 개가 여러 line 뒤에 나올 수 있습니다.

곤약젤리복숭아
최강록명란
합계수량/금액
1
1
1,900
1,700

이 구조에서 한 item의 name과 amount를 하나의 excerpt로 묶으려면 block 대부분을 인용해야 합니다.

cap을 여덟 줄로 늘리면 통과할 수 있겠지만, whole-page quote에 가까워집니다.

현재 contract는 다른 해법을 씁니다.

item에 nameEvidenceamountEvidence를 따로 둡니다.

각각 자기 line에서 검증하고, 별도의 position pairing check로 순서와 재사용을 확인합니다.

cap은 local하게 인접한 한 claim을 위한 도구입니다.

떨어진 column 사이의 관계까지 cap 하나로 해결하려 하면 abstraction이 과하게 커집니다.

fail-first로 예전 기대가 실제로 깨지는지 확인했습니다

이번 글을 쓰며 먼저 예전 cap의 기대를 일부러 넣었습니다.

assert.equal(verifyEvidence(fourLineExcerpt, page), false);

현재 구현에서는 assertion이 실패했습니다.

actual: true
expected: false

그 다음 실제 acceptance를 확인했습니다.

four-line quote: verified
five-line quote: rejected
four OCR frames: enclosed into one box

focused test도 함께 실행했습니다.

node --test packages/contract/test/guards.test.ts packages/contract/test/anchor.test.ts
20 tests passed

이 test set은 empty excerpt, non-adjacent splice, 3-line과 4-line item, 5-line rejection, ambiguous box, multi-line enclosing rectangle을 읽습니다.

test가 “multi-line을 지원한다”는 모양만 보는 것이 아니라 cap과 adjacency, ambiguity라는 보호 속성을 각각 확인합니다.

좋은 guard는 거절 횟수가 아니라 설명 가능한 경계를 만듭니다

한 줄 규칙은 엄격했습니다.

하지만 실제 영수증을 거절하는 엄격함은 제품에서 신뢰로 이어지지 않았습니다.

반대로 line 수를 무제한으로 풀어주는 것도 신뢰가 아닙니다.

page 전체를 evidence라고 부를 수 있게 만들 뿐입니다.

현재 경계는 작고 구체적입니다.

  • line은 연속되어야 한다.
  • excerpt는 최대 네 line이다.
  • 네 line은 실제 capture가 요구한 만큼이다.
  • 더 넓혀야 한다면 새로운 실제 capture가 필요하다.
  • verify와 anchor는 같은 line-run 후보를 본다.
  • box 위치가 ambiguous하면 임의로 고르지 않는다.
  • 멀리 떨어진 column은 더 큰 cap이 아니라 split evidence로 다룬다.

이 규칙의 좋은 점은 숫자 4 자체가 아닙니다.

1이 부족했고, 왜 무제한이 위험하며, 무엇이 다음 변경을 정당화하는지 설명할 수 있다는 점입니다.

guard는 많이 막을수록 좋은 벽이 아닙니다.

실제 입력을 통과시키면서도 거짓이 빠져나갈 구멍을 어디까지 닫았는지 증명할 수 있어야 합니다.

제 경우 그 경계는 한 줄이 아니라, 관측된 네 개의 인접 line이었습니다.