anydoc: 오피스 문서를 LLM이 읽을 Markdown으로 5밀리초 안에 바꾸는 Rust 라이브러리

anydoc 소개

문서를 입력으로 받는 AI 파이프라인은 대체로 같은 자리에서 막힙니다. 사용자가 올리는 파일은 .docx, .pptx, .xlsx, .pdf, .epub처럼 제각각인데, 언어 모델에 넣으려면 결국 하나의 텍스트 표현으로 정리해야 합니다. 그래서 형식마다 다른 파서를 붙이게 되고, 표 하나가 docx에서는 멀쩡히 나오는데 rtf에서는 깨지는 식의 불일치가 쌓입니다. 표 이스케이핑 버그를 고쳐도 그 수정이 다른 형식에는 적용되지 않아 같은 작업을 형식 개수만큼 반복하게 되고, 변환기 자체가 무거우면 문서 한 건에 1초 넘게 걸려 수집 단계 전체가 느려집니다.

anydoc은 파서는 형식별로 두되 그 뒤를 하나로 합치는 방식으로 이 문제에 접근합니다. 각 형식 파서는 공통 문서 모델(Document)을 만들어 내고, 그 모델 하나만 GitHub Flavored Markdown 직렬화기를 통과합니다. 이스케이핑, 표, 제목 앵커, 각주가 어떤 형식에서 들어왔든 동일하게 처리되는 이유이며, 저자들은 이 구조 덕분에 "docx의 표 이스케이핑 수정이 곧바로 rtf, odt를 비롯한 나머지 전부의 수정이 된다" 고 설명합니다. 구현은 순수 Rust이고 머신러닝 모델이나 외부 서비스를 호출하지 않아, 문서 한 건 변환 시간 중앙값이 5밀리초 아래입니다.

만든 곳은 웹 크롤링 API로 알려진 Firecrawl이며, 이 라이브러리는 자사 서비스인 Firecrawl Parse를 구동하는 엔진이기도 합니다. 배포 형태는 Rust 크레이트, Node.js 패키지, Python 휠, 그리고 브라우저용 WebAssembly 빌드까지 네 가지이고, 별도 설치 없이 쓰는 CLI와 코딩 에이전트용 Agent Skill도 함께 제공됩니다. 현재 최신 릴리즈는 v0.1.6입니다.

anydoc과 기존 문서 변환 도구의 차이

문서를 마크다운으로 바꾸는 도구는 이미 여럿 있습니다. LibreOffice를 헤드리스로 돌리는 방식은 지원 형식이 넓은 대신 프로세스 기동 비용이 크고, Microsoft의 markitdown이나 pandoc은 가볍지만 다루는 형식이 제한적입니다. docling과 unstructured는 문서 이해 쪽에 초점이 있어 변환만 필요한 경우에는 과합니다.

저장소에는 이 도구들과 anydoc을 직접 비교한 벤치마크가 공개되어 있습니다. 14개 형식에 걸친 실사용 문서 100건을 대상으로 했고, 품질 점수는 LibreOffice로 렌더링한 문서 첫 6페이지 이미지를 정답으로 두고 두 도구의 출력을 블라인드 비교하는 방식으로 매겼습니다. 심사는 Claude Sonnet 5가 맡았으며, 위치 편향을 상쇄하기 위해 모든 쌍을 출력 순서를 바꿔 두 번씩 판정해 총 482건의 판정을 모았습니다.

도구 지원 형식 변환 중앙값 종합 점수
anydoc 14/14 4.4ms 81
mammoth 1/14 52.5ms 70
markitdown 6/14 134.8ms 65
unstructured 8/14 572.9ms 63
docling 4/14 513.6ms 57
pandoc 5/14 102.1ms 56
libreoffice 12/14 1129.5ms 40

수치를 읽을 때 주의할 점이 하나 있습니다. 종합 점수는 각 도구가 지원하는 형식들에 대한 평균이라, mammoth의 70은 docx 한 종류만의 점수인 반면 anydoc의 81은 14개 형식 전체의 평균입니다. 저자들도 이 점을 짚으면서 형식별 표가 공정한 비교라고 밝히고 있는데, 그 형식별 표에서는 anydoc이 13개 형식 모두에서 가장 높은 점수를 받았습니다. 속도는 Ryzen 9 9950X3D 환경에서 문서당 한 번의 웜 변환을 측정한 값이고, anydoc과 Python 라이브러리는 프로세스 기동 시간을 제외한 반면 CLI 도구는 포함했습니다. 벤치마크 하네스는 저장소의 bench/ 디렉토리에 있지만 문서 코퍼스 자체는 재배포가 불가능해 포함되어 있지 않습니다.

anydoc은 누구에게 유용한가

여러 형식이 섞여 들어오는 문서 수집 파이프라인을 운영한다면 얻는 것이 분명합니다. 형식별 파서를 각각 관리하다가 하나로 줄일 수 있고, 출력이 형식과 무관하게 일정해 후속 청킹이나 인덱싱 규칙을 한 벌만 두면 됩니다. 브라우저에서 파일을 서버로 보내지 않고 변환해야 하는 경우에도 WebAssembly 빌드가 그대로 해법이 됩니다.

반대로 스캔 문서가 주된 입력이라면 맞지 않습니다. anydoc은 머신러닝 모델을 쓰지 않아 이미지로만 이루어진 PDF는 Unsupported 오류로 돌려보내며, 이런 문서에는 별도의 OCR 단계가 필요합니다. 원본의 좌표나 폰트 같은 레이아웃 정보까지 보존해야 하는 작업, 그리고 이미 docx 하나만 다루면서 기존 파서에 만족하고 있는 경우에도 교체 이득이 크지 않습니다.

anydoc의 동작 원리

변환 경로는 형식 감지, 형식별 파싱, 공통 모델, 직렬화의 네 단계로 이어집니다. 저장소가 제시하는 전체 흐름은 다음과 같습니다.

document bytes
  │
  ├─► format detection      → content markers, not the extension
  │
  ├─► format parser          → one per format (doc, docx, ppt, pptx, xls,
  │                            xlsx, odt/ods/odp, rtf, epub, csv)
  │         │
  │         └─► Document     → shared model: blocks, inlines, tables,
  │                            footnotes, assets
  │               │
  │               └─► GFM serializer → Markdown
  │
  └─► PDF → pdf-inspector    → Markdown directly

형식 감지는 확장자가 아니라 파일 내용을 근거로 합니다. PDF 헤더, RTF 여는 그룹, OLE 스트림 이름, ZIP 패키지의 mimetype처럼 각 규격이 지정한 표식을 바이트에서 직접 읽기 때문에, 확장자가 잘못 붙은 파일도 올바르게 변환됩니다. 다만 CSV에는 그런 표식이 없어서 확장자나 명시적인 형식 지정이 필요합니다.

Format::from_bytes(&bytes); // Some(Format::Docx), or None when nothing matches
Format::from_extension("pptm"); // Some(Format::Pptx)
Format::from_path(Path::new("report.odt")); // Some(Format::Odt)

공통 문서 모델이 담는 범위는 좁지 않습니다. 앵커가 붙은 제목, 굵게/기울임/취소선, 인라인 코드와 코드 블록, 링크와 문서 내 상호 참조, 원본의 번호 매김을 그대로 따르는 중첩 목록과 체크리스트, 병합 셀과 헤더 행이 있는 표, 블록 인용, 각주와 미주, 발표자 노트까지 들어갑니다. 이미지와 임베드된 객체는 마크다운에서 대체 텍스트로 표현되고, 원본 바이트는 미디어 타입 정보와 함께 문서 모델에 남아 있어 필요하면 따로 꺼내 쓸 수 있습니다.

PDF만 경로가 다릅니다. 같은 조직이 만든 pdf-inspector를 거쳐 곧바로 마크다운이 되며, 텍스트 기반 PDF는 OCR 서비스 없이 로컬에서 처리됩니다.

변환이 실패했을 때 무엇 때문인지도 오류 타입으로 구분됩니다. ConvertError 는 알 수 없거나 변환할 수 없는 형식(Unsupported), 구조적으로 사용 불가(Malformed), 암호화됨(Encrypted), 안전 한계 초과(ResourceLimit), 필수 파트 부재(MissingPart), 파일 읽기 실패(Io)를 각각 다른 변형으로 알려 줍니다. 대량 처리 중 특정 파일만 건너뛰고 나머지를 계속 돌리는 코드를 쓰기 쉬운 구조입니다.

anydoc이 지원하는 문서 형식

형식 확장자
Word .doc, .docx, .docm
PowerPoint .ppt, .pps, .pot, .pptx, .pptm, .ppsx, .ppsm
Excel .xls, .xlsx, .xlsm, .xlsb
OpenDocument .odt, .ods, .odp
Rich Text Format .rtf
EPUB .epub
CSV .csv
PDF .pdf

anydoc 설치 및 사용법

가장 빠른 경로는 CLI입니다. npx 가 첫 실행에서 플랫폼에 맞는 사전 빌드 바이너리를 내려받으므로 별도 설치 없이 바로 쓸 수 있습니다.

npx @firecrawl/anydoc report.docx               # Markdown to stdout
npx @firecrawl/anydoc slides.pptx -o slides.md  # or to a file
npx @firecrawl/anydoc - --format csv < data.csv # read stdin

Python에서는 파일 경로와 바이트 양쪽을 받습니다. 바이트를 넘기면 내용에서 형식을 감지하고, CSV처럼 표식이 없는 형식은 이름을 함께 지정합니다.

import anydoc

# From a file path:
markdown = anydoc.to_markdown("report.docx")

# From bytes, with the format detected from the content:
markdown = anydoc.to_markdown_bytes(data)

# Or name it, which signature-less formats (CSV) need:
markdown = anydoc.to_markdown_bytes(data, "csv")

# Or stop at the document model, which also carries embedded assets:
document = anydoc.to_document(data)

Node.js API도 구성이 같습니다.

import { toDocument, toMarkdown, toMarkdownBytes } from '@firecrawl/anydoc';

// From a file path:
const markdown = await toMarkdown('report.docx');

// From bytes, with the format detected from the content:
const fromBytes = await toMarkdownBytes(bytes);

// Or stop at the document model, which also carries embedded assets:
const document = await toDocument(bytes);

바인딩이 호스트 런타임을 막지 않도록 손을 봐 둔 점도 눈여겨볼 만합니다. Node.js 변환은 libuv 스레드 풀에서 돌아 이벤트 루프를 막지 않고, Python은 GIL을 해제해 다른 스레드가 계속 진행됩니다. TypeScript 타입 정의와 Python 스텁은 패키지에 함께 들어 있습니다.

코딩 에이전트에 붙이려면 Agent Skill 형태로 설치합니다. 이 스킬은 에이전트에게 anydoc CLI로 문서를 변환하는 방법을 알려 주며, Claude Code, Codex, Cursor, OpenCode 등에서 동작합니다.

npx skills add firecrawl/anydoc

설치 없이 결과부터 보고 싶다면 공식 데모 페이지에서 바로 확인할 수 있습니다. 이 페이지는 라이브러리를 WebAssembly로 실행하므로 파일이 브라우저 밖으로 나가지 않습니다.

anydoc의 라이선스

anydoc은 MIT 라이선스로 공개되어 있어 개인 및 상업적 목적으로 자유롭게 사용할 수 있습니다.

:framed_picture: anydoc 브라우저 데모

:github: anydoc 프로젝트 GitHub 저장소

더 읽어보기




이 글은 GPT 모델로 정리한 초안을 바탕으로 한 것으로, 원문의 내용 또는 의도와 다르게 정리된 내용이 있을 수 있습니다. 관심있는 내용이시라면 원문도 함께 참고해주세요! 읽으시면서 어색하거나 잘못된 내용을 발견하시면 댓글로 알려주시기를 부탁드립니다. :hugs:

이 도구를 직접 설치해 사용해보셨다면, :pytorch:파이토치 한국 사용자 모임:south_korea: 회원들을 위해 경험이나 팁을 댓글로 남겨주세요! :folded_hands: