feat(marker): handwritten 자동 skip — Phase 1D pilot 결과 반영
1D pilot (2026-05-02 야간 sweep, 25 controlled_backfill 결과) 에서
필기 PDF 3건 (4798 / 4813 / 4815) 이 status='success' 로 변환됐으나
사용자 quality 평가에서 좋은 자료 추출 불가 판정. 근본 원인은 Marker
설정 부족이 아니라 입력 자체 (애플펜슬 손글씨 + 사용자 글씨체 = OCR/
layout 모델 한계 영역). Marker 튜닝으로 해결될 영역이 아니므로 enqueue
단계에서 자동 skip.
가드 로직:
marker_worker.process() 의 doc_type SKIP 직후 (1.5 단계) title/path 의
보수적 키워드 4개 (필기, 손글씨, handwritten, handwriting) 매칭 시
_set_skipped() 호출. md_content/md_content_hash NULL clear,
md_extraction_error='skipped: handwritten note (title/path heuristic)',
content_origin='extracted'.
키워드 선정 (보수적):
포함: 필기 / 손글씨 / handwritten / handwriting
제외 (false positive 위험):
- 노트 (노트북 매뉴얼 / release notes / Note_240528_워크숍 같이
필기 아닌 정상 문서까지 잡음)
- scan / 스캔 (스캔 PDF 中 정상 변환되는 케이스 있음, 1D 결과
doc 5127 표준기계설계(KS)_08_핀 density 1.59 / scan_likely 인데
성공)
logger:
markdown_skip_handwritten_hint id=<id> keyword=<matched> title=<...>
regex 단위 테스트 15 케이스 (실 production fastapi venv) 전부 통과:
매칭: Note_240805_용접교육 필기 / Note_240827_필기 / 손글씨 모음 /
Handwritten Notes 2024 / handwriting practice / path/필기/* /
path/handwritten_collection/* (8건)
비매칭: 다이아프람워크숍 / 노트북 매뉴얼 / Release notes v2 / PIPE
FABRICATORS / 표준기계설계 / scan documentation / 스캔 문서 (7건)
이번 가드는 enqueue 시점 적용. 이미 success 인 4건의 md_content 는
보존 (사용자가 직접 보고 싶을 때 표시 가능). 정리 필요 시 별건.
후속 (별 PR):
- A2 (정식 doc_type='필기노트' 라벨): 1D 3건 sample 너무 적어 라벨
정의 보류. 필기 PDF 누적 후 별도 검토.
- C (Phase 2 풀 backfill plan): 본 PR 머지 후 별도 라운드.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,25 @@ SKIP_DOC_TYPES = {
|
|||||||
"Invoice", "Purchase_Order", "Estimate", "Statement",
|
"Invoice", "Purchase_Order", "Estimate", "Statement",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Phase 1D 결과 반영 (2026-05-03):
|
||||||
|
# 애플펜슬 손글씨 필기 PDF 는 사용자 글씨체 + Marker layout/OCR 모델 한계로
|
||||||
|
# quality fail. Marker 튜닝 영역이 아니라 input 자체가 markdown 부적합. title/path
|
||||||
|
# 에 명시적 필기 표식이 있으면 enqueue 단계에서 자동 skip.
|
||||||
|
#
|
||||||
|
# 보수적 키워드 4개만 (false positive 회피):
|
||||||
|
# 포함: 필기, 손글씨, handwritten, handwriting
|
||||||
|
# 제외: 노트 (노트북 매뉴얼 / release notes), scan/스캔 (5127 처럼 정상 변환되는
|
||||||
|
# 스캔 PDF 가 있어 false positive 위험)
|
||||||
|
SKIP_HANDWRITTEN_KEYWORDS = ("필기", "손글씨", "handwritten", "handwriting")
|
||||||
|
HANDWRITTEN_HINT_REGEX = re.compile("|".join(SKIP_HANDWRITTEN_KEYWORDS), re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _matches_handwritten_hint(title: str | None, file_path: str | None) -> str | None:
|
||||||
|
"""title 또는 file_path 에 보수적 필기 키워드 매칭 시 매칭 키워드 반환, 아니면 None."""
|
||||||
|
blob = " ".join(filter(None, [title or "", file_path or ""]))
|
||||||
|
m = HANDWRITTEN_HINT_REGEX.search(blob)
|
||||||
|
return m.group(0) if m else None
|
||||||
|
|
||||||
# documents.file_path 는 NAS 상대경로 (예: 'news/SCMP/abc').
|
# documents.file_path 는 NAS 상대경로 (예: 'news/SCMP/abc').
|
||||||
# fastapi 와 marker-service 모두 NAS 를 /documents 에 ro 마운트.
|
# fastapi 와 marker-service 모두 NAS 를 /documents 에 ro 마운트.
|
||||||
CONTAINER_PATH_PREFIX = os.getenv("MARKER_CONTAINER_PATH_PREFIX", "/documents")
|
CONTAINER_PATH_PREFIX = os.getenv("MARKER_CONTAINER_PATH_PREFIX", "/documents")
|
||||||
@@ -76,6 +95,19 @@ async def process(document_id: int, session: AsyncSession) -> None:
|
|||||||
await _set_skipped(session, document_id, f"skipped: doc_type={doc.document_type}")
|
await _set_skipped(session, document_id, f"skipped: doc_type={doc.document_type}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# ---- (1.5) handwritten hint skip (Phase 1D pilot 결과 반영) ----
|
||||||
|
matched_keyword = _matches_handwritten_hint(doc.title, doc.file_path)
|
||||||
|
if matched_keyword:
|
||||||
|
logger.info(
|
||||||
|
f"markdown_skip_handwritten_hint id={document_id} "
|
||||||
|
f"keyword={matched_keyword} title={(doc.title or '')[:80]}"
|
||||||
|
)
|
||||||
|
await _set_skipped(
|
||||||
|
session, document_id,
|
||||||
|
"skipped: handwritten note (title/path heuristic)",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# ---- (2) file_path validation ----
|
# ---- (2) file_path validation ----
|
||||||
if not doc.file_path:
|
if not doc.file_path:
|
||||||
await _fail(session, document_id, "no file_path")
|
await _fail(session, document_id, "no file_path")
|
||||||
|
|||||||
Reference in New Issue
Block a user