Files
doc2md/convert.py
minsung 2ec2759a20 feat: Implement full conversion pipeline (PDF/HWP/HWPX/HML/HTML)
- convert.py: 통합 CLI, --json 출력, --scan 폴더 모드
- converters/pdf.py: 페이지별 분류(text/diagram/mixed) + marker-pdf + PNG 렌더링
- converters/hwp.py: COM 자동화 + pyhwp fallback
- converters/hwpx.py: ZIP+XML 직접 파싱, 이미지 추출
- converters/hml.py: XML 파싱, Base64 이미지 추출, colspan/rowspan HTML 표
- converters/html.py: html2text (body_width=0)
- requirements.txt: 최소 의존성
- .env.example: 환경변수 템플릿

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

137 lines
4.7 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
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()