Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/fig-extract-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const seeds = toFigureEntries(res, (p) => pageHeights[p]);
- **번호 글리프에 ToUnicode 매핑이 없는 PDF는 원리상 미탐지**: 번호가 화면에는 정상으로 보이는데 텍스트 레이어에 문자가 없는 문서가 있다(Wiley 일부). 엔진이 아니라 PDF 쪽 문제라 사용자 눈에는 "번호가 멀쩡히 보이는데 안 잡힌다"로 보인다 — 문의가 오면 수동 크롭 안내가 맞다.
- **영역 경계 정밀화 (v2.10.x)**: figure/table·나란한 컬럼 경계 판정을 개선했다 — table 캡션을 **경계로만** 인식해 인접 figure 크롭에서 table을 제외(v2.10.0, table 자체 방출은 없음), 좌우로 나란한 두 figure가 서로를 통째로 크롭하던 것을 각자 캡션 컬럼으로 분리(v2.10.1 같은 baseline, v2.10.2 baseline 어긋난 offset). 출력 필드·좌표계·(num,page) 식별자 불변 — bbox가 더 타이트해질 뿐이라 소비자 코드 변경은 불요.
- **캡션 문법 확대 (v2.11.0)**: 보충·부록 캡션의 inline 표기를 새로 잡는다 — `Fig. S1.`·`Figure S1:`·`Figure A1.`(문자접두 번호), `Supplemental`/`Supporting Figure N`(접두), `FIG. 3 (color online).`·`Figure 1 (저자명).`(괄호 한정구). 전부 **점형 canonical**(`S.N`·`A.N`)으로 방출하므로 `num` 필드에 `"S.1"`·`"A.1"` 형태가 더 자주 등장한다(v2.6.0의 `ED.N`·prefix `S.N`과 동일한 표기 규약 — 새 값 형태 아님). 출력 필드·좌표계·(num,page) 식별자·manifest 스키마 불변, 소비자 코드 변경 불요. 주의: 한 물리 figure의 캡션에 다른 계열 라벨이 중첩된 오제출 문서(예: Extended Data 캡션 본문에 `Figure S1.`)는 같은 그림을 `ED.N`+`S.N` 두 번 방출할 수 있다(candidate suppression 미구현 — 엔진 repo 백로그, n=1 코너).
- **전면 figure 크롭 개선 (v2.12.0)**: Nature Extended Data류 **전면(full-page) figure**가 과대 패널티에 눌려 페이지 일부만 크롭되던 것을 해소했다 — 전면 figure의 크롭 영역이 더 정확(전체)해진다. 출력 필드·좌표계·(num,page)·manifest 스키마 불변, 소비자 코드 변경 불요(bbox가 truth에 더 가까워질 뿐).
- 엔진은 백그라운드 탭에서 크롬 타이머 스로틀링의 영향을 받는다(분석이 수십 배 느려짐).
전체 문서 스캔은 사용자가 뷰어를 보고 있는 동안 idle로 돌리는 것을 권장.

Expand Down
23 changes: 20 additions & 3 deletions src/core/fig-extract.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@

const FigExtract = (() => {

const VERSION = "2.11.0";
const VERSION = "2.12.0";
// 2.12.0: [계약 무변경] hugePenalty 전면 figure 오발 완화 — Nature Extended Data류 전면 figure(up 영역이
// 페이지의 65~78% 차지)가 hugePenalty(-8)를 맞아 healthy(≥8) 문턱 아래로 눌리면, ⓐ up 점수 직접
// 하락 + ⓑ detached-with-up side 거부(up≥8 조건)가 꺼져 작은 side 크롭에 짐 → clip. **up 방향이고
// bodyStops=0(본문 stop 없음)이며 (farClosed || !raster)일 때 hugePenalty 면제**한다 — 진짜 전면
// figure는 far 경계가 닫혔거나 벡터로 채워지고, `!farClosed && raster`는 경계 안 닫힌 영역이 raster
// 너머로 over-reach한 것이라 벌점 유지(Simões 4@6 칼럼폭 over-grab 회귀 방지). body>0·side는 무변경.
// 전수 diff 회귀 감사(v2.11.0↔): gate FIX 8 critical clip(Xue ED.1/2/6/9·Simões ED.9·Luques 3/ED.3·
// Paul ED.4) + Structural 13@14 보너스, gate REGRESS 0. measureCandidate가 metrics.direction을 실어
// figureScore가 up 한정 판정.
// 2.11.0: [계약 무변경] 캡션 문법 확장 (M1 점없는 문자+숫자 번호 · M2 괄호 한정구) — 보충/부록 캡션의
// 모든 표면표기를 탐지해 canonical(점형 S.N·A.N·ED.N)로 방출한다(EVAL §4.5 정체성 규약). 세 갈래:
// ① inline 문자+숫자 — "FIG. S1." "Fig. S1:" "Figure S1" "Figure S.1" "Figure A1"(점 유무·구분자
Expand Down Expand Up @@ -673,7 +682,14 @@ function figureScore(m) {
const bodyPenalty = Math.min(6, m.bodyStops * 1.5);
const tinyPenalty = (m.areaRatio < 0.002 || m.heightRatio < 0.012) ? 8 : 0;
const slenderPenalty = (m.widthRatio < 0.10 && m.heightRatio > 0.12) ? 5 : 0;
const hugePenalty = (m.areaRatio > 0.65 || m.heightRatio > 0.82) ? 8 : 0;
/* hugePenalty: 영역이 페이지를 통째로 삼키는 over-grab 방지. 단 up 방향이고 bodyStops=0이며
(farClosed || !raster)일 때 면제한다(v2.12.0) — 본문을 문 흔적 없는 진짜 전면 figure(Nature
Extended Data류)는 far 경계가 닫혔거나(farClosed) 벡터로 영역을 채운다(raster=0). 반면
`!farClosed && raster`는 경계가 안 닫힌(ink가 far 경계까지 이어지는) 영역 안에 raster figure가
든 형태 = raster 너머로 over-reach한 것이라 벌점 유지(Simões 4@6 회귀 방지). over-grab은
본문을 삼켜 bodyStops>0이므로 여전히 벌점, side 후보도 무변경(full-height 보호 유지). */
const hugeExempt = m.direction === "up" && m.bodyStops === 0 && (m.farClosed || !m.raster);
const hugePenalty = (!hugeExempt && (m.areaRatio > 0.65 || m.heightRatio > 0.82)) ? 8 : 0;
Comment on lines +691 to +692

@coderabbitai coderabbitai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

up 후보의 실제 body stop 수를 hugeExempt에 전달해야 합니다.

Line 691-692는 m.bodyStops === 0일 때 hugePenalty를 면제하지만, scan()은 Line 952에서 unprotectedBodyStops: 0을 항상 반환합니다. 따라서 upStopReason === "body"인 과대 후보도 bodyStops가 0으로 남아 farClosed 또는 벡터 조건에 의해 벌점을 면제받을 수 있습니다. scan()이 body stop 시 stopNstop을 반환하도록 수정한 뒤 이 값을 metrics에 전달해야 합니다.

수정 예시
-      return { incl, rx0, rx1, stopReason, stopNstop, farBlankPx, unprotectedBodyStops: 0 };
+      return { incl, rx0, rx1, stopReason, stopNstop, farBlankPx,
+        unprotectedBodyStops: stopReason === "body" ? stopNstop : 0 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/fig-extract.js` around lines 691 - 692, Update scan() to return
stopNstop when the scan ends because of a body stop, then propagate that value
into the metrics used by the hugeExempt calculation. Ensure up candidates with
upStopReason === "body" no longer appear as bodyStops === 0, while preserving
the existing hugePenalty logic for other candidates.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v2.12.1에서 패치

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@onetwothr1 알겠습니다. v2.12.1에서 scan()의 실제 body-stop 수를 metrics로 전달하는 패치가 반영되면 확인하겠습니다. 현재 PR에서는 해당 수정이 아직 포함되지 않았으므로 코멘트는 열린 상태로 두겠습니다.

const otherCapPenalty = m.stopReason === "other-cap" ? 0.5 : 0;
const total = area + width + height + density + proximity + boundary + raster
- bodyPenalty - tinyPenalty - slenderPenalty - hugePenalty - otherCapPenalty;
Expand Down Expand Up @@ -976,7 +992,8 @@ function detectPage(pg, lines, dom, grid, dbg, captionData) {
farBlankPx,
farClosed: farBlankPx >= Math.round(4.8 * S),
raster,
stopReason
stopReason,
direction: dir
};
};
const scoreText = (dir, candidate) => {
Expand Down