Files
hyungi_document_server/scripts/poc_office_md.py
T
hyungi 68e2d7ea04 feat(documents): S1-ADD dedup·원본명 3컬럼 + md_status success→completed 매핑 (A) + office→md PoC (C-1)
plan ds-s1-backend-1 (r5 수렴). 코드만 스테이징 — migration 미적용(restart 보류, E-2 Soft Lock 예외창).

A (앱 v1 디코딩 비파괴 최소선):
- A-1 migrations/287_documents_dedup_fields.sql: original_filename TEXT / duplicate_of BIGINT FK ON DELETE SET NULL
  / duplicate_count INTEGER NOT NULL DEFAULT 0. 단일 statement·PG16 fast-path·BEGIN/COMMIT 금지. backfill 미포함(B-4).
- A-2 app/models/document.py: 1계층 블록에 3 mapped_column (+ ForeignKey import). md_* 는 기존.
- A-3 app/api/documents.py: DocumentResponse 3필드(duplicate_count=0 non-opt) + DocumentDetailResponse
  field_validator(success→completed, mode=before) — read-time DB→API 단방향, write(ORM) 미적용.
- A-4 tests/test_s1_dedup_shape.py: success→completed 동작 + 비-success 통과 + 3필드 디폴트/roundtrip
  + ds-app contract fixture 디코드(skip-if-absent). py_compile OK. ★ backend 절반 — 전체 비파괴는 S3 render 테스트와 AND.

C-1 PoC (워커 미연결 — C-2 에서 marker_worker 분기 연결):
- app/workers/office_md.py: OOXML=markitdown(신규 dep, lazy) / hwp·hwpx=LibreOffice headless→HTML→markdownify(기존 dep).
  실패·빈출력·타임아웃·dep부재 → OfficeMdError raise (success+빈md 금지 = C-5 postcondition 의 변환기 계약).
- scripts/poc_office_md.py: 표 fidelity 측정 하니스. E-1 = prod LibreOffice 버전핀 안전컨텍스트 실행(hwpx 필터 버전 의존).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 03:05:30 +00:00

78 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""C-1 PoC 하니스 — office/hwp → md 변환 품질(특히 표 fidelity) 측정.
plan ds-s1-backend-1 C-1/E-1:
- hwp/hwpx 결과는 LibreOffice 버전 의존 → **prod extract_worker 와 동일 버전(버전핀 안전컨텍스트)** 에서 실행해야
신호가 transfer 됨. live worker 에 job 태우는 것 아님(점유 0).
- OOXML 은 markitdown(신규 dep): `pip install markitdown`.
- 샘플은 trivial 말고 **대표 복잡본**(법령·KGS 표 중심 .hwp/.hwpx, 병합셀/다중시트 xlsx).
사용:
python scripts/poc_office_md.py <file_or_dir> [<file_or_dir> ...]
# 예: 현 코퍼스 백필 후보(doc/docx/xls/xlsx/hwp) 샘플 디렉토리
python scripts/poc_office_md.py ~/poc_samples/
각 파일: 변환 성공 시 char/표 행수/heading 지표 + 본문 미리보기.
실패(OfficeMdError) 시 FAILED 출력 — 이것이 C-5 가 md_status='failed' 로 라우팅할 케이스(설계대로).
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
# app/ 를 path 에 (모듈 import 용).
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app"))
from workers.office_md import SUPPORTED, OfficeMdError, convert_office_to_md, table_fidelity # noqa: E402
def _iter_targets(args: list[str]):
for a in args:
p = Path(a).expanduser()
if p.is_dir():
for child in sorted(p.rglob("*")):
if child.is_file() and child.suffix.lower() in SUPPORTED:
yield child
elif p.is_file():
yield p
else:
print(f" (skip, 경로 없음: {p})")
def main(argv: list[str]) -> int:
if not argv:
print(__doc__)
return 2
targets = list(_iter_targets(argv))
if not targets:
print("변환 대상(.docx/.xlsx/.pptx/.hwp/.hwpx) 없음.")
return 1
ok = fail = 0
for path in targets:
print(f"\n=== {path.name} ({path.suffix.lower()}) ===")
try:
md = convert_office_to_md(path)
except OfficeMdError as e:
fail += 1
print(f" FAILED → (C-5 가 md_status='failed' 라우팅) : {e}")
continue
ok += 1
fid = table_fidelity(md)
print(f" OK chars={fid['chars']} lines={fid['lines']} "
f"table_rows={fid['table_pipe_rows']} (sep≈표수 {fid['table_separator_rows']}) "
f"heading={fid['has_heading']}")
preview = "\n".join(f" | {ln}" for ln in md.splitlines()[:12])
print(preview)
print(f"\n--- 합계: OK {ok} / FAILED {fail} / 총 {len(targets)} ---")
print("표 fidelity 가 낮으면(table_rows 0 등) 해당 포맷은 변환기/필터 재검토 — "
"OOXML↔markitdown, hwp/hwpx↔LibreOffice 경계를 데이터로 확정(C-1).")
return 0 if fail == 0 else 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))