From 7d0fca267da787d77cb93dd51ef003faf231869b Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Sun, 3 May 2026 08:11:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(marker):=20handwritten=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20skip=20=E2=80=94=20Phase=201D=20pilot=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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= keyword= 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) --- app/workers/marker_worker.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/workers/marker_worker.py b/app/workers/marker_worker.py index 904ae10..20302ee 100644 --- a/app/workers/marker_worker.py +++ b/app/workers/marker_worker.py @@ -39,6 +39,25 @@ SKIP_DOC_TYPES = { "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'). # fastapi 와 marker-service 모두 NAS 를 /documents 에 ro 마운트. 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}") 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 ---- if not doc.file_path: await _fail(session, document_id, "no file_path")