diff --git a/app/requirements.txt b/app/requirements.txt index 3ccc782..8451495 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -21,6 +21,10 @@ pymupdf>=1.24.0 trafilatura>=1.12.0 readability-lxml>=0.8.1 markdownify>=0.13.1 -# office OOXML(docx/xlsx/pptx) → md (plan ds-s1-backend-1 C-1). hwp 는 LibreOffice+markdownify 경로. +# office OOXML(docx/xlsx/pptx) → md (plan ds-s1-backend-1 C-1). # 정확한 핀은 E-1 markitdown OOXML PoC(devsbx/버전핀 컨텍스트)에서 확정. markitdown[docx,xlsx,pptx]>=0.1.0 +# .hwp(HWP5 binary) → md: 순수 Python HWP5 전용 변환기(CLI hwp5html). LibreOffice 번들 libhwplo +# 필터가 실제 한컴 HWP5 를 못 읽어 전건 실패 → pyhwp 로 교체(2026-06-09). six = pyhwp 의 미선언 런타임 의존성. +pyhwp>=0.1b15 +six>=1.16.0 diff --git a/app/workers/marker_worker.py b/app/workers/marker_worker.py index c9b5634..60cb273 100644 --- a/app/workers/marker_worker.py +++ b/app/workers/marker_worker.py @@ -396,8 +396,13 @@ async def _process_office( """ from workers.office_md import OfficeMdError, convert_office_to_md - is_hwp = Path(container_path).suffix.lower() in (".hwp", ".hwpx") - engine = "libreoffice_hwp" if is_hwp else "markitdown" + suffix = Path(container_path).suffix.lower() + if suffix == ".hwp": + engine = "pyhwp" # HWP5 binary: libhwplo 못 읽어 pyhwp 로 교체(2026-06-09) + elif suffix == ".hwpx": + engine = "libreoffice_hwp" # HWPX 는 pyhwp 미지원 → LibreOffice 폴백 + else: + engine = "markitdown" try: # 동기 subprocess(LibreOffice)/markitdown — 스레드로 빼서 이벤트 루프 비차단. md_content = await asyncio.to_thread(convert_office_to_md, container_path) diff --git a/app/workers/office_md.py b/app/workers/office_md.py index 32c5728..4345955 100644 --- a/app/workers/office_md.py +++ b/app/workers/office_md.py @@ -5,9 +5,11 @@ 전략 (하이브리드): - OOXML(.docx/.xlsx/.pptx) → markitdown ← 신규 의존성(pip install markitdown). lazy import. - - .hwp/.hwpx → LibreOffice(headless) → HTML → markdownify ← markdownify 기존 의존성. - (LibreOffice 가 hwp import 필터 보유. .hwpx 는 .hwp 와 다른 필터·버전 의존 → E-1: prod LibreOffice - 버전핀 안전컨텍스트에서 PoC 실행. 표 fidelity 가 진짜 리스크 — 하니스가 측정.) + - .hwp(HWP5 binary) → pyhwp hwp5html → HTML → markdownify ← pyhwp+six 의존성. + (2026-06-09: LibreOffice 번들 libhwplo 필터가 실제 한컴 HWP5 파일을 못 읽어 rc=0 + 'source file + could not be loaded' 로 전건 실패 → 순수 Python HWP5 전용 변환기 pyhwp 로 교체.) + - .hwpx → LibreOffice(headless) → HTML → markdownify ← markdownify 기존 의존성. + (HWPX(zip)는 pyhwp 미지원 → LibreOffice 폴백 유지. 현재 코퍼스는 전부 HWP5 binary.) 실패 계약 (C-5 postcondition 의 backend 절반): 변환 실패·빈 출력·타임아웃·의존성 부재 → OfficeMdError 를 raise 한다. @@ -18,6 +20,7 @@ from __future__ import annotations import os +import re import shutil import subprocess import tempfile @@ -34,6 +37,9 @@ _MIN_BODY_CHARS = 16 # 이름) → 기본값 정합. soffice 만 있는 환경은 LIBREOFFICE_BIN 으로 override. _SOFFICE_BIN = os.environ.get("LIBREOFFICE_BIN", "libreoffice") +# pyhwp 콘솔 스크립트(pip install pyhwp 시 PATH 등록). HWP5 binary(.hwp) 전용. +_HWP5HTML_BIN = os.environ.get("HWP5HTML_BIN", "hwp5html") + class OfficeMdError(Exception): """office/hwp → md 변환 실패 신호. 호출부는 md_status='failed' 로 라우팅.""" @@ -50,7 +56,9 @@ def convert_office_to_md(path: str | Path, *, timeout: int = 90) -> str: if suffix in OOXML_FORMATS: md = _via_markitdown(p) - else: # .hwp / .hwpx + elif suffix == ".hwp": + md = _via_pyhwp_html(p, timeout=timeout) + else: # .hwpx (pyhwp 미지원 → LibreOffice 폴백) md = _via_libreoffice_html(p, timeout=timeout) md = (md or "").strip() @@ -74,8 +82,51 @@ def _via_markitdown(path: Path) -> str: return getattr(result, "text_content", "") or "" +def _via_pyhwp_html(path: Path, *, timeout: int) -> str: + """HWP5 binary(.hwp) → pyhwp hwp5html → markdownify. + + LibreOffice 번들 libhwplo 필터가 실제 한컴 HWP5 파일을 못 읽어(rc=0 + 'source file could + not be loaded') 전건 실패 → 순수 Python HWP5 전용 변환기 pyhwp(CLI hwp5html)로 교체. + `_via_libreoffice_html` 와 동일한 실패 계약(rc≠0 또는 출력 부재 → OfficeMdError raise). + """ + try: + from markdownify import markdownify # 기존 의존성 + except ImportError as e: # noqa: BLE001 + raise OfficeMdError("markdownify 미설치(기존 의존성이어야 함)") from e + + with tempfile.TemporaryDirectory(prefix="office_md_hwp_") as tmp: + outdir = Path(tmp) + # hwp5html --output