- 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>
141 lines
4.8 KiB
Python
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()
|