Files
doc2md/convert.py
minsung 1d40d90242 fix: LaTeX 백슬래시 복원, HWP 인코딩 오류 수정, 다이어그램 감지 튜닝
- pdf.py: marker-pdf가 손상시킨 \times·\frac 등 LaTeX 백슬래시 복원 후처리 추가
- pdf.py: 다이어그램 감지에 절대 drawing 수 기준(>= 40) 추가 (대형 엔지니어링 페이지 대응)
- hwp.py: COM 타임아웃 메시지의 em dash → ASCII (cp949 인코딩 오류 수정)
- convert.py: Windows stdout/stderr UTF-8 강제 설정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 15:56:11 +09:00

141 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""
doc2md — 통합 문서 변환기 (AI 에이전트용)
사용법: python convert.py <file> -o <output_dir> [--json]
python convert.py --scan <dir> -o <output_dir> [--json]
자세한 사용법: AGENT_GUIDE.md 참조
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
if sys.platform == 'win32':
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
SUPPORTED = {'.pdf', '.hwp', '.hwpx', '.hml', '.html', '.htm'}
SKIP_NAMES = {'README.md', 'CLAUDE.md', 'AGENT_GUIDE.md'}
def convert_file(src: Path, output_dir: Path) -> dict:
"""파일 하나를 변환. AGENT_GUIDE 스펙 dict 반환."""
ext = src.suffix.lower()
try:
if ext == '.pdf':
from converters.pdf import convert_pdf
return convert_pdf(src, output_dir)
elif ext == '.hwp':
from converters.hwp import convert_hwp
return convert_hwp(src, output_dir)
elif ext == '.hwpx':
from converters.hwpx import convert_hwpx
return convert_hwpx(src, output_dir)
elif ext == '.hml':
from converters.hml import convert_hml
return convert_hml(src, output_dir)
elif ext in {'.html', '.htm'}:
from converters.html import convert_html
return convert_html(src, output_dir)
else:
return {"status": "skipped", "input": str(src), "reason": "unsupported_format"}
except Exception as e:
return {"status": "error", "input": str(src), "error": str(e)}
def scan_and_convert(scan_dir: Path, output_dir: Path) -> dict:
"""폴더 스캔 후 변환 대상 일괄 처리."""
targets = []
for ext in SUPPORTED:
targets.extend(scan_dir.rglob(f'*{ext}'))
targets.sort()
results = []
ok = fail = skipped = 0
for src in targets:
if src.name in SKIP_NAMES:
continue
# 이미 .md 존재하면 스킵
if src.with_suffix('.md').exists():
results.append({"input": str(src), "output": None,
"status": "skipped", "reason": "already_md"})
skipped += 1
continue
out_dir = output_dir / src.parent.relative_to(scan_dir)
r = convert_file(src, out_dir)
results.append(r)
if r['status'] == 'ok':
ok += 1
elif r['status'] == 'error':
fail += 1
else:
skipped += 1
return {
"status": "ok" if fail == 0 else ("error" if ok == 0 else "partial"),
"total": len(results),
"converted": ok,
"skipped": skipped,
"failed": fail,
"results": results,
}
def main():
parser = argparse.ArgumentParser(
description='doc2md — AI 에이전트용 문서 변환기',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='자세한 사용법: AGENT_GUIDE.md'
)
parser.add_argument('file', nargs='?', help='변환할 파일')
parser.add_argument('-o', '--output', required=True, help='출력 폴더')
parser.add_argument('--scan', metavar='DIR', help='폴더 일괄 변환 모드')
parser.add_argument('--json', action='store_true', help='결과를 JSON으로 출력')
args = parser.parse_args()
output_dir = Path(args.output)
if args.scan:
result = scan_and_convert(Path(args.scan), output_dir)
exit_code = 0 if result['status'] == 'ok' else (1 if result['status'] == 'partial' else 2)
elif args.file:
src = Path(args.file)
if not src.exists():
err = {"status": "error", "input": str(src), "error": "파일 없음"}
if args.json:
print(json.dumps(err, ensure_ascii=False))
else:
print(f"오류: 파일 없음 — {src}", file=sys.stderr)
sys.exit(2)
result = convert_file(src, output_dir)
exit_code = 0 if result['status'] == 'ok' else 2
else:
parser.print_help()
sys.exit(1)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
# 사람이 읽기 쉬운 출력 (에이전트가 --json 없이 호출 시)
status = result.get('status', '')
if 'results' in result:
print(f"[doc2md] {result['converted']}개 변환 / {result['skipped']}개 스킵 / {result['failed']}개 실패")
else:
output = result.get('output', '')
print(f"[doc2md] {status.upper()}{output or result.get('error', '')}")
if result.get('has_diagrams'):
pages = result.get('diagram_pages', [])
print(f"[doc2md] 다이어그램 페이지: {pages} → Vision AI 처리 필요")
sys.exit(exit_code)
if __name__ == '__main__':
main()