@shuntagami/markitdown-node
v0.6.0
Published
資料を忠実な Markdown に変換する Node.js ライブラリと CLI
Readme
@shuntagami/markitdown-node
事業者の資料を、原文に忠実な Markdown に変換する Node.js ライブラリと CLI。実行には Node.js 24 以上が必要です。Python・ブラウザ・外部の変換サーバーは必要ありません。
インストール
任意のディレクトリから CLI を実行できます。
npm install -g @shuntagami/markitdown-node
markitdown --help
markitdown ./資料.pdf --output ./資料.mdインストールせずに実行する場合は npx @shuntagami/markitdown-node ./資料.pdf --output ./資料.md。
AI を使う変換には GEMINI_API_KEY などの設定が必要です。テキストだけの変換は markitdown ./資料.txt --no-ai で実行できます。
アプリケーションに組み込む場合は npm install @shuntagami/markitdown-node。
ES modules 形式で、TypeScript の型定義を同梱しています。利用側に pnpm や TypeScript のインストールは不要です。
開発
pnpm install
pnpm check
pnpm exec node dist/cli.js --help開発環境は Node.js 26.13.0 / pnpm 12.16.1 / TypeScript 7.0.2。2026-10-29 時点の最新安定版を確認し、直接依存は完全なバージョンで固定しています。devEngines.runtime により、pnpm がプロジェクト用の Node.js を用意します。ライブラリと CLI の実行は Node.js 24 以上に対応します。
更新時は pnpm up --latest と pnpm check。pnpm outdated で確認します。間接依存は各ライブラリの対応範囲で解決し、強制的なメジャーバージョン上書きは行いません。
品質チェック
ever-better で ESLint の既存指摘をベースラインとして記録し、新たな指摘の増加を pnpm lint / pnpm check と CI で止めます。整形は引き続き Biome、型に基づく検査は typescript-eslint、複雑度・不具合候補は SonarJS と security plugin が担当します。
行数・関数長・複雑度の制約も有効です。既定の目安は 1 ファイル 600 行、1 関数 50 行、循環的複雑度・認知的複雑度 15、ネスト 4 段階です(行数は空行とコメントを除外)。指摘ゼロを目標にせず、実際の不具合や保守性の改善を優先します。 合理的な構造に対する指摘は、eslint.config.js でファイルとルールを限定し、理由付きの例外や閾値を設定して構いません。ルール全体を外したり、数合わせで分割したりする必要はありません。ever-better 1.0 は eslint-disable で免除した指摘も件数に含めるため、コメントによる免除は無効にし、例外を設定に集めています。
pnpm quality:check # ベースラインを変更せず、指摘の増加を検査
pnpm quality:report # 既存の指摘を一覧化(CI のジョブサマリにも出力)
pnpm quality:prune # 修正で不要になった免除を回収
pnpm knip # 未使用コードの調査(指摘は警告)
pnpm duplication # src 内の重複を調査(重複率では CI を落とさない)eslint-suppressions.json・.ever-better/state.json・QUALITY.md は一緒に管理します。修正・例外化の後は quality:prune で更新し、CI を通すためだけに freeze --force で上限を上げないでください。ベースラインは「ファイル × ルールの指摘件数」であり、既に長さ制限を超えている関数がさらに長くなった量までは検知しません。既存の大きな箇所は変更時にも構造を確認します。詳細な運用は AGENTS.md にあります。
TypeScript 7 は現在の typescript-eslint が利用する旧 API を持たないため、公式の併用方法を使っています。@typescript/native は [email protected] の alias で tsc を提供し、typescript は @typescript/[email protected] の alias として lint 用 API と tsc6 を提供します。pnpm exec tsc --version は引き続き 7.0.2 です。
ever-better diagnose は Biome や pnpm check 内の間接呼び出しを認識しないため、formatter や CI が不足と表示される場合があります。導入済みのコマンドと CI の実行結果で確認し、診断の項目を埋めるためだけに重複したツールやワークフローを追加しません。
CLI
markitdown ./資料.pdf \
--env-file /path/to/existing/.env \
--output /path/outside/repository/資料.md \
--json /path/outside/repository/資料.result.json \
--checkpoint /path/outside/repository/資料.checkpoint.jsonpnpm を使う場合も pnpm add -g @shuntagami/markitdown-node でインストールできます。
- Gemini は
gemini-3.8-flashを使用。--modelで変更できます。別モデルへの自動切り替えはありません。 - キーは
GEMINI_API_KEY、GOOGLE_GENERATIVE_AI_API_KEY、GOOGLE_API_KEYのいずれか。プロセスの環境変数を.envより優先します。.envから読み取った値を環境全体へ反映したり、鍵を保存したりしません。 --no-aiでは AI を呼びません。画像が必要な箇所はvision-requiredとして明示します。- Markdown は
--outputに保存、未指定時は標準出力。進捗と処理時間は標準エラー出力です。 - 終了コード:
0全部成功、2部分成功、1失敗、130中断。 - 同じ
--checkpointで再実行すると、同じ資料・モデル・設定で成功した部分を再利用します。失敗部分だけ再実行します。Ctrl+C でも完了済み部分を残します。 --pdf-mode autoが標準です。visionは全ページを画像で読み、textは明示的に文字抽出だけを行います。textモードでは画像内の情報を読みません。
原本、出力、チェックポイントは別のパスにしてください。クライアント資料とその変換結果はリポジトリ外に保存します。
ライブラリ
入力・出力・成否・ログインや権限エラーの契約は API 仕様書 を参照してください。
import { convert } from "@shuntagami/markitdown-node";
import { createGeminiVision } from "@shuntagami/markitdown-node/gemini";
const result = await convert(
{ bytes: pdfBytes, name: "資料.pdf" },
{
vision: createGeminiVision({ apiKey: process.env.GEMINI_API_KEY! }),
concurrency: 3,
signal: abortController.signal,
resume: previousResult,
async onProgress({ completed, total, part, checkpoint }) {
// checkpoint を永続化し、completed / total を画面へ返す。
await saveCheckpoint(checkpoint);
},
},
);
// AI へ渡す内容と、原本の隣へ保存する内容を同じにする。
await saveMarkdown(result.markdown);
if (result.status === "success") {
await readWithYourAI(result.markdown);
} else {
showProblems(result.issues);
}入力は { path }、{ url }、{ bytes, name?, mimeType? }。形式は実データから判定し、拡張子や MIME 指定を根拠にパーサーを選びません。mimeType は呼び出し側のメディアプロバイダーに渡す補足です。
返す Markdown の先頭には、出典と取得・変換日時を YAML frontmatter で付けます。
---
source-url: "https://example.com/material.pdf"
retrieved-at: "2026-10-29T09:00:00.000Z"
converted-at: "2026-10-29T09:01:00.000Z"
source-format: "pdf"
conversion-status: "success"
---source-url: 最初に渡された URL。Google の書き出し先やリダイレクト先には置き換えません。ユーザー名・パスワード、token・API キー・署名など既知の認証パラメーターは、Markdown とsource.urlの両方から除きます。実際の取得には渡された URL を使用します。- JSON の
source.finalUrlには取得先、または文字起こしへ委ねるメディアの解決先を返します。source.resolvedUrlは書き出し変換前の資料 URL です。どちらも既知の認証パラメーターを除いた保存用の値です。Markdown のsource-urlは引き続き入力 URL です。 source-file: ファイル入力では URL の代わりに元ファイル名を記録します。{ path }ではファイル名だけ、{ bytes, name }では渡されたnameのファイル名部分を使います。nameのない生データでは推測せず省略します。retrieved-at: ライブラリが HTTP で元資料の本文を取得し終えた時刻。ローカルファイル・生データ・外部に委ねる文字起こしでは実際のダウンロード時刻が分からないため付けません。取得自体に失敗した場合も省略します。converted-at: 変換処理を終えた時刻。進捗途中のチェックポイントでは省略し、最終結果に付けます。source-format/conversion-status: 判定した形式とsuccess/partial/failed。Markdown だけを開いても、一部未読かどうかを確認できます。
日時は UTC の ISO 8601 表記です。JSON にも同じ値を source.retrievedAt / convertedAt で返します。再開時は今回の取得・変換日時を記録します。
入力 Markdown に有効な YAML frontmatter があれば、既存の title などを保って統合します。上記6つの出典管理キーは今回の値で更新します。不正な YAML や、置き換えるキーへのエイリアス参照などで安全に統合できない場合は、元の YAML 全体を本文としてそのまま残します。本文の字下げや改行、parts[].markdown の原文は保持します。
戻り値は常に markdown を持ちます。読めなかった部分にもページ位置と理由を残します。status は処理の完了状況であり、AI の転記が人手確認不要という意味ではありません。無効な設定値は呼び出しの誤りとして例外にします。
parts は原本の順序、onProgress は完了した順序です。進捗コールバックは同時実行せず、保存を待ってから次へ進みます。費用・トークン数の集計と返却は行いません。
任意の画像モデルを利用する場合は { id, read(request) } という VisionProvider を渡せます。id はモデル・プロンプト・設定変更時に更新します。CLI でも --provider ./providers.mjs から export default { vision, transcription, captions, browser } を渡せます。これは利用者が指定するコードであり、資料中のコードは実行しません。
読み取り方
| 入力 | 処理 |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| PDF | ページ単位。文字のほかに何も描かれていないページ(ページ全面の背景は除く)は抽出。画像・図形・線・段組み・重複文字のあるページは画像と文字ヒントをモデルへ |
| PNG / JPEG / WebP | 描写や要約をせず、見える文字を転記 |
| Word (.docx) | 段落・見出し・表を変換し、埋め込み画像も読み取り |
| HTML / URL | 本文と画像の出現順を保持。遅延画像・srcset・HTML 内で指定された背景画像も対象 |
| UTAGE | elements-component の埋め込み JSON を解析。JavaScript の実行・ブラウザ操作はしない |
| Google ドキュメント | 認証不要の HTML 書き出し。全タブの本文・表・埋め込み画像を順に処理 |
| Google スライド | 公開の PDF 書き出しを PDF として処理 |
| Google スプレッドシート | 公開の XLSX 書き出しから全シートを名前付きの表へ |
| テキスト / Markdown / CSV / JSON / XML | テキストを保持。UTF-8、BOM 付き UTF-16 に対応 |
| 音声・動画 / YouTube 等 | 呼び出し側の文字起こしプロバイダーへ。字幕プロバイダーを渡した場合は字幕を先に試す |
| Canva / ログインが必要なページ | 読み取り失敗と、PDF 書き出し・共有変更の案内 |
| PowerPoint | PDF 書き出しを案内 |
| Excel ファイル / ZIP / Notebook / その他のバイナリ | 非対応を明示 |
音声・動画処理の実体は既存の elevenlabs-transcribe-bot の Cloud Run API を使います。このパッケージに ffmpeg、ワーカー、API サーバー、認証情報は追加しません。API ジョブの受付・完了待ち・結果取得は呼び出し側のプロバイダーの役割です。実サービスへの接続は別作業です。
URL の事前確認
記事本文の抽出(0.3.0)
一般の HTML では、標準の contentMode: "auto" が記事メタ情報や本文構造を確認し、Mozilla Readability で記事本文を抽出してから Markdown 化・画像読み取りを行います。X・note・Zenn などのサイト名で分岐する仕組みではありません。プロフィール、関連記事、ナビの画像は本文候補に含まれなければ AI に送りません。本文中に著者が書いた宣伝文や引用を意味で削る処理ではありません。
const result = await convert({ url }, { contentMode: "auto", vision });
// result.source.contentScope: "article" または "page"
// result.source.title: 抽出した記事のタイトル(任意)記事の根拠がないページや UTAGE の LP は全体を維持します。必ず記事本文だけに絞りたい場合は contentMode: "article"、全体を読みたい場合は "page" を指定できます。CLI は --content-mode auto|article|page です。Google の書き出し・Word・PDF 等の資料処理は変えません。
記事と判断したのに本文を特定できないときは article-not-found を返し、ページ全体を本文として返しません。ブラウザを渡した場合は描画後にも同じ抽出を行います。選択した領域内のコード・図表・画像・埋め込みの脱落を検出すると article-incomplete を残します。抽出は推定なので、全サイトで完全な範囲を保証するものではありません。比較結果は 記事抽出の検証 に記録します。
Readability 用 DOM はスクリプト実行・外部リソース読み込みを無効のまま使います。DOM 要素は50,000件まで。Node 24 などの環境で require(ESM) が無効でも動作するよう jsdom 26.1 を使用しています。
開けるかだけの確認(0.5.0)
import { probe } from "@shuntagami/markitdown-node";
const check = await probe(url); // 変換しない。AI もブラウザも動かさない
// check.status: ok / failed、check.issues: convert と同じ理由コード共有設定の誤りなどを、変換を待たずに知るための関数です。URL の分類・転送・Google の共有・ログイン・404 などは convert と同じ判定で、本文は HTML 以外は先頭 64 KB しか読みません。25 枚の Google スライドで 13〜23 秒 → 1.5 秒でした。ok は読み取りの成功保証ではありません。詳しくは API 仕様 の 5 章です。
読み取り予定と実績
import { assessMaterial, convert } from "@shuntagami/markitdown-node";
const result = await convert({ url }); // 画像 AI を呼ばない
const check = assessMaterial(result);
// check.state: ready / partial / deferred / failed
// check.textChars: 出典情報や診断メッセージを除く抽出本文の字数
// check.deferredParts / needsVision: 画像 AI 待ち
// check.unreadParts / issues: 動画など、別途対応が必要な部分result.status は今回の読取実績です。本文がなく画像だけが未処理なら failed のまま、事前確認は deferred(「画像は後で読み取り予定」)になります。画像 17 枚と埋め込み動画 1 件なら deferredParts: 17、unreadParts: 1 です。deferred は将来の成功保証ではありません。呼び出し側が vision-required を取り除いて状態を推測する必要はありません。
0.2.1 から、本文の判定は Markdown の構文を解釈し、コード例のコメントを保持しながら、実際の HTML コメント・画像リンク・代替テキストを本文量から除きます。画像 AI が画像リンクだけを返した部分は empty として未読にします。画像に文字がないことを確認した場合は <!-- visual: 文字なし --> を返してください。この部分は確認済みとして扱いますが、入力全体から本文が得られなければ結果は failed / empty、文字数は0です。別に本文がある場合、文字なし画像だけを理由に部分失敗にはしません。変換済み Markdown 自体は判定のために書き換えず、プレーンテキスト・XML などのテキスト入力も原文として扱います。旧版で欠落したコード例を再利用しないよう、チェックポイントの変換設定識別子も更新しています。
短縮 URL は HTTP 転送の各段で再分類します。Google の書き出し、Drive の open?id=、Dropbox の dl=0、Canva のリンクにも同じ処理を使います。YouTube のチャンネル・再生リスト、SNS プロフィール、Drive フォルダは、それぞれ youtube-channel / youtube-playlist / social-profile / drive-folder を返し、一覧画面を本文にしません。Canva のデザインは引き続き canva、公開 *.canva.site は通常の Web ページとして扱います。
Google の「ウェブに公開」形式は、文書では公開本文、スライドでは全スライドの HTML 表示、表では公開一覧に含まれる各シートを読みます。公開スライドの図形の位置関係は完全には再現できないため unsupported-content を残します。公開表はビューアーのタイトルだけを本文にせず、公開された各シートを見出し付きの表にします。
音声・動画を非同期処理へ渡す場合は onMediaUrl(url) で解決後の URL を受け取れます。音声・動画の Content-Type が返った場合、巨大な本体を先にダウンロードしません。transcription / captions にも解決後の URL が渡ります。署名付き URL は保存用の source.finalUrl では復元できないため、このコールバックの値を使ってください。認証パラメーターを含む場合があるのでログや公開結果に残さず、必要なら非公開のジョブ保存先で扱います。
任意のブラウザ
options.browser を渡した場合、一般の HTML ページはまずブラウザで描画し、描画後の画面を本文として読みます。素の HTML の文面から「器かどうか」を推測しません(0.4.0。JavaScript が中身を作るページを、読み込み中の案内だけで「読めた」にしないため)。描画できなかったときだけ素の HTML を読みます。Google の書き出しと、静的 HTML に全内容を JSON で持つ UTAGE は描画しません。描画後も同じ本文判定と画像 AI を使います。ブラウザを渡さない場合、短い文字しかなくスクリプトが中身を作る形のページは javascript-required(読めていない)として返します。
import { createPlaywrightBrowser } from "@shuntagami/markitdown-node/playwright";
const result = await convert(
{ url },
{
browser: createPlaywrightBrowser(),
vision,
maxImages: 64,
},
);参考実装を使う環境でのみ pnpm add playwright と pnpm exec playwright install chromium を実行してください。CLI は markitdown URL --browser。ブラウザを渡さなければ Playwright のインストールも起動も不要です。遠隔の箱を使う場合は BrowserProvider の render({ url, signal, timeoutMs, maxBytes, maxTotalBytes }) を実装し、{ html, finalUrl, issues? } を返します。プロバイダーの id は実装・設定変更時に更新します。
参考実装は匿名の一時 Chromium を呼び出しごとに閉じます。networkidle は使わず、DOM の安定と上限付きのスクロールを待ちます。Notion は公開ページを対象にし、notion.so の外部サイト案内から *.notion.site のリンクへ進みます。子ページやデータベース全件をたどらず page-scope を返します。Canvas、iframe、仮想スクロール、折りたたまれた内容などの完全な取得を保証するものではありません。
ブラウザの通信境界
通常の URL 検査だけでは、ブラウザが後から読み込む画像・スクリプト・iframe・API 通信を守れません。参考実装では次を組み合わせています。
- Chromium 全体に、接続を拒否するローカルプロキシを設定。ルーティングを通らない通信を直接外へ逃がさず、localhost の暗黙バイパスも無効化します。
- HTTP(S) リソースは
route.fulfillへ渡す前に Node 側で取得。リダイレクトは自動追跡せず、各宛先とソケットが使う DNS 応答を検査します。route.continue()による再名前解決には委ねません。 - Service Worker を無効化し、WebSocket を遮断。QUIC と WebRTC の非プロキシ UDP を抑止します。
- 呼び出し全体の時間、取得量、リクエスト数、同時通信数を制限。遮断・上限到達は結果の
issuesに残します。
独自プロバイダーもこの境界を担います。プロバイダーに渡す前後の publicUrl 検査だけで安全だとは扱わないでください。サーバーでは秘密情報を持たない隔離環境と、ネットワーク出口での内部宛先の拒否も組み合わせてください。このリポジトリは箱の作成やデプロイを行いません。
HTML の画像は、明確なナビ・装飾要素を除き、本文候補として扱います。長い本文があるというだけで画像を捨てません。標準は 64 枚までで、超えた画像は image-limit として未読に残します。LP などでは maxImages / CLI の --max-images で上限を変更できます。PDF の画像読み取りはこの HTML 用上限の対象外です。
本家から改善する点
- 空の画像 PDF、ログイン画面、JavaScript の殻を成功扱いにしない。
- 文字数が多くても、画像や複雑な配置の情報を落としそうな PDF は画像を読む。
- 写真の説明ではなく文字の転記を指示。資料中の命令はデータとして扱う。
- ページ別の成功・失敗、理由、処理時間を返す。モデルの出力打ち切りも失敗扱い。
- 部分的な失敗を残し、成功部分を再利用。同じ HTML 画像への AI 呼び出しをまとめながら、原文の登場位置は維持。
- URL のリダイレクト先と、実際に接続に使う DNS 応答を検査。内部アドレス、IPv4-mapped IPv6、ユーザー情報付き URL を拒否。
- HTTP ボディを逐次読み込み、取得中にバイト上限を適用。Office の ZIP 展開量や巨大なシートも制限。
MarkItDown の現行 PDF 変換には表抽出の改善もあります。単なる機能の移植ではなく、資料内の情報と構造を維持する観点で実装・検証します。
上限と現在の制約
- 標準: 1 入力・画像 32 MiB、取得合計 128 MiB、500 部分、HTML 画像 64 枚、並列 3、HTTP 30 秒。
ConvertOptionsで変更可能。 - PDF の標準レンダリングは最大 800 万画素。内部キャンバスは 3200 万画素まで。PDF に埋め込まれた JavaScript は実行しません。
- ブラウザを渡さない場合、外部 CSS や任意の SPA の実行は行いません。取得できない動的要素・canvas・iframe・動画などは未完了として残します。静的な本文が取れても、これらが残れば
partialです。 - スプレッドシートは保存済みの値を読むため、数式の再計算、グラフ、画像、表示書式の再現は対象外。数値は文字列として読み、長い番号を丸めません。日付は ISO 表記になります。数式のキャッシュがない場合は失敗を返します。
- Google ドキュメントは
tab=を外した HTML 書き出しで全タブを取得します。タブ内容は連結され、タブ UI 固有の名前・階層は書き出しにない場合は再現しません。画像 AI を渡さない場合、画像はvision-requiredです。 - Word のヘッダー・フッター・埋め込み図表、スプレッドシートの画像・図表を検出した場合、読み取れた本文を返しつつ
unsupported-contentで部分成功にします。 - アニメーション画像、複数ページ TIFF、旧 Word
.docは非対応。失敗を返して黙って先頭だけ読むことを避けます。 - チェックポイントは信頼できる保存先から渡してください。原本バイト列と設定を照合します。HTML の画像は再取得して内容を照合します。動画 URL の内容が更新された場合は
--no-resumeで再処理します。 - Vercel では Node 24 以上の関数から利用します。ネイティブ依存
@napi-rs/canvasをデプロイへ含め、アプリのバンドルと実行時間を実環境で確認してください。ZERO の置換・直接アップロード対応は別 PR です。
検証と配布
pnpm test は API キーやネットワークを使わず、合成資料で形式判定・画像 PDF・複雑な PDF・部分失敗・再開・HTML・Word・複数シート・URL 安全性・CLI を確認します。CI は Linux と macOS の Node 24・26 でテストと配布物の動作を確認します。型チェック・lint・ビルドは開発用の Node 26 で実行します。
初期の検証結果は docs/validation.md、URL の前後比較は docs/url-validation.md、呼び出し側の移行は docs/url-materials-handoff.md。再比較には node scripts/compare.mjs reference.md result.json を使えます。本文を出力せず、ページ別の一致指標だけを返します。
pnpm pack --pack-destination /path/outside/repository で配布内容を確認できます。配布物は dist・README・検証記録・LICENSE・package.json に限定しています。CI では配布物を別ディレクトリへインストールし、公開 API と CLI の動作も確認します。
npm では @shuntagami/markitdown-node を public として配布します。package.json のバージョンを更新した PR を main にマージすると、.github/workflows/ci.yml が Node 24・26 の全チェック成功後に自動公開します。公開済みのバージョンはスキップします。失敗時は Actions の CI → Run workflow から main を再実行できます。
認証は npm Trusted Publishing(GitHub: ele-inc/markitdown-node、workflow: ci.yml)を使い、npm トークンの登録や公開ごとのパスキー認証は不要です。GitHub リポジトリは非公開のまま利用でき、そのため provenance の生成は無効にしています。prepublishOnly で pnpm check、prepack でビルドが実行されます。ZERO への導入は別途行います。
MIT ライセンス。依存も MIT / Apache / BSD / ISC / Zlib 系です。JSZip の二重ライセンスは MIT を選択します。pnpm licenses list で確認できます。
