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>
This commit is contained in:
136
convert.py
Normal file
136
convert.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user