マークダウンファイルからプレゼンテーション動画を自動生成するアプリケーションです。 Cloudflareのエコシステム(Workers, Durable Objects, R2)をフル活用したサーバーレスアーキテクチャを採用しています。
- Markdown to Video: シンプルなMarkdownファイルからスライドと音声を生成し、動画化します。
- Serverless Architecture: Cloudflare Workers + Durable Objects + R2 で構築され、待機コストはほぼゼロです。
- Scale to Zero: 動画生成を行うコンテナはオンデマンドで起動し、アイドル時に自動停止します。
- No External DB: Redisなどの外部データベースを使わず、Cloudflare Durable Objectsでジョブキューと状態を管理します。
- ライブプレビュー: Markdown入力時にブラウザ上でスライドのリアルタイムプレビューを生成・表示します。これにより、最終的な動画のイメージを素早く確認できます。
- Framework: Vue.js 3
- Build Tool: Vite
- Hosting: Cloudflare Pages
- Runtime: Cloudflare Workers
- Framework: Hono
- Language: TypeScript
- Job Queue & State: Cloudflare Durable Objects (Redis不要)
- Storage: Cloudflare R2 (動画・音声ファイル)
- Cache: Cloudflare Workers KV
- Runtime: Cloudflare Containers (Docker)
- Tools: Node.js, FFmpeg, Puppeteer
- TTS: VOICEVOX (Docker)
完全なサーバーレスアーキテクチャを採用し、コスト効率とスケーラビリティを最大化しています。
- API Gateway: Cloudflare Workers がリクエストを受け付け。
- Job Queue: Durable Objects がジョブの順序とステータスを管理。
- Storage: 素材と生成物は Cloudflare R2 に保存。
- Compute: 重い動画生成処理はコンテナで行い、Workersからオンデマンドで起動。
- Realtime: WebSocket (Durable Objects) で進捗をリアルタイム通知。
詳細なアーキテクチャ解説は 技術ブログ記事 をご覧ください。
- Node.js v18+
- pnpm
- Docker (コンテナ動作確認用)
# 依存関係インストール
pnpm install
# Cloudflare Workers (Backend) 起動
pnpm dev:workers# コンテナビルド & 起動
docker-compose -f docker-compose.cloudflare.yml up --buildCLIモードでは、ローカル環境でプレゼンテーション動画を生成できます。
input/ディレクトリにMarkdownファイル(.md)とテキストファイル(.txt)を配置pnpm dev:cliを実行
CLIモードでは、事前に用意された画像や無音動画ファイルを input/ に置くことで活用できます:
- スライド画像:
input/{番号}__{タイトル}.png,.jpg,.jpegが存在する場合、Markdownからの画像生成をスキップ - 無音動画:
input/{番号}__{タイトル}.nosound.mp4が存在する場合、無音動画の生成をスキップ
これにより、外部ツールで作成した高品質な画像やカスタム動画を利用できます。
CLI(apps/cli)でのローカルメディア生成フローに関する重要な仕様と、追加した便利な機能をまとめます。
- 入力場所: 事前準備素材はすべて
input/に配置します。ファイル名はNNN__title.*の形式を想定します(例:020__slide-title.md,021__cover.png,031__demo.mp4)。 - スライド素材の自動利用:
- 画像:
input/{id}__*.png|jpg|jpegがある場合は Markdown レンダリングをスキップしてその画像をスライドに使用します。 - 無音動画:
input/{id}__*.nosound.mp4を置くとスライドごとの無音動画生成をスキップできます。 - フル mp4:
input/{id}__*.mp4を置くと、音声の有無を検査してそのまま使うか(音声あり)、音声と合成するか(無音)を自動で判断します。
- 画像:
- 出力ファイル: 各スライドの最終動画は
output/{id}__title.mp4、さらに音声と結合した「処理済みファイル」をoutput/{id}__title.processed.mp4として出力します。最終的な結合はoutput/final_presentation.mp4に書き出されます。 - 音声と動画のマージ:
- スライドごとに音声(VOICEVOX生成)と動画を結合する際、音声の先頭が負のPTSになるケースに対処するため、マージ処理で音声PTSをリセット(
asetpts=PTS-STARTPTS)します。 - マージ時は「長い方の長さを優先」するロジックを取り入れています(audio/video をプローブして短い方を
apad/tpadで延長し-tで長さを揃える)。 - まず可能なら高速なコピーパス(ストリームコピー)を試行し、問題があれば再エンコードして正常化します。
- スライドごとに音声(VOICEVOX生成)と動画を結合する際、音声の先頭が負のPTSになるケースに対処するため、マージ処理で音声PTSをリセット(
- 最終結合(concat):
- デフォルトは高速な concat(copy ベース)を試み、失敗や音声の欠落があれば再エンコードでフォールバックします。
- 再エンコード concat は PTS 再生成フラグ(
-fflags +genpts -avoid_negative_ts make_zero)を使い、必要に応じて全入力を同一解像度/コーデックに正規化します。 - 再エンコード時は規定解像度(デフォルト
1920x1080)に「アスペクト比を維持してフィット(scale+pad)」するフィルタを適用します。
開発中にローカルでメディア生成(FFmpeg / Puppeteer)をデバッグしやすくするため、いくつかの環境変数を用意しています。必要に応じてオン/オフしてログの詳細を切り替えてください。
SLIDE_RENDER_KEEP_HTML=true: 一時的に作成される Puppeteer 用の HTML をoutput/temp_slide_<timestamp>.htmlのように保持します。inliner の動作確認や手動検証に便利です。SLIDE_RENDER_DEBUG=true: スライド HTML のローカル画像解決(inliner)がどの候補パスを探索したか等のデバッグログを有効にします。FFMPEG_VERBOSE=true: デフォルトでは FFmpeg の大量出力(フレーム情報等)は抑制されています。詳細な進行ログが必要な場合はこの値をtrueにして実行してください。
例: 詳細ログを有効にして ID 020 を処理する
export SLIDE_RENDER_KEEP_HTML=true SLIDE_RENDER_DEBUG=true FFMPEG_VERBOSE=true
pnpm cli 020例: 通常は FFmpeg の冗長ログを抑えて実行する
export SLIDE_RENDER_KEEP_HTML=true SLIDE_RENDER_DEBUG=true
unset FFMPEG_VERBOSE
pnpm cli 020これらのフラグは packages/core 内のユーティリティによって参照されます(packages/core/src/services/slide_template.ts と packages/core/src/utils/ffmpeg.ts)。
Markdown 内で相対パスの画像(例: )を使う場合、Puppeteer が file:// から HTML を読み込むためそのままだと画像が解決されないことがあります。本プロジェクトでは HTML を Puppeteer に渡す前に、相対画像を data:URI に変換する inliner を実装しました。
- 処理箇所:
packages/core/src/services/slide_template.ts - 動作:
<img src="./foo.png">を探索し、見つかればdata:image/...;base64,...に置換して HTML を返します。 - 探索順序(既定):
baseDirが呼び出し元から渡されていればその中を優先process.cwd()からの解決- リポジトリルート(
pnpm-workspace.yamlまたは上位のpackage.jsonを基準)内のinput/ディレクトリ - 最後の手段として短い深さでリポジトリ内を basename 検索
このロジックにより、モノレポの各パッケージで CLI を実行しても input/ 下の素材が見つかるようにしています。より確実にしたい場合は、renderSlide の呼び出し元で明示的に baseDir を渡すことを推奨します。
-
画像からの動画化:
imageToVideo(スライド画像 → 無音動画)は、画像をアスペクト比を維持してターゲット解像度にフィット(中央パディング)するscale+padフィルタを使用します。デフォルト解像度は1920x1080です。 -
CLI 引数の便利機能:
pnpm --filter @presentation-maker/cli dev: すべて生成 → 結合(従来の動作)pnpm --filter @presentation-maker/cli dev 020: 指定のスライド ID(複数可)だけ処理して結合pnpm --filter @presentation-maker/cli dev final: 既に存在するoutput/*.processed.mp4を使って「最終結合のみ」を行う(再生成は行いません)
-
サムネイル→PDF バッチ:
apps/cliにthumbnails-to-pdfスクリプトを追加しました。output/*.processed.mp4の先頭フレームを抽出してoutput/presentation_thumbnails.pdfにまとめます。
# すべての処理
pnpm --filter @presentation-maker/cli dev
# ID 020 のみ処理
pnpm --filter @presentation-maker/cli dev 020
# 既存の processed 動画だけ結合して final を作る
pnpm --filter @presentation-maker/cli dev final
# processed の先頭サムネイルを PDF にまとめる
pnpm --filter @presentation-maker/cli run thumbnails-to-pdfこのセクションで説明した CLI とメディアパイプラインの実装は、packages/core/src/utils/ffmpeg.ts(および生成済みの dist)にあるユーティリティを使って行われています。音声PTSの正規化やアスペクト比維持、再エンコードフォールバックなどのロジックはそこで管理されています。
presentation_maker/
├── apps/
│ ├── cli/ # CLIツール
│ ├── server/ # ローカル開発用バックエンド (Node.js/Express)
│ ├── web/ # フロントエンド (Vue.js)
│ └── workers/
│ ├── api/ # Cloudflare Workers (API Gateway)
│ └── container/ # 動画生成ワーカー (Cloudflare Container)
├── packages/
│ └── core/ # 共通ロジック (動画生成, Voicevoxクライアントなど)
├── input/ # CLI用入力ファイル
└── output/ # CLI用出力ファイル
Cloudflareへのデプロイ手順は Cloudflare デプロイガイド を参照してください。
Cloudflare Containersへのデプロイは、イメージのビルド、タグ付け、プッシュ、そしてWorker設定の更新まで1つのコマンドで自動化されています。
# コンテナとWorkerをデプロイ
pnpm deploy:containerこのコマンドは自動的に日時ベースのタグを生成し、wrangler.jsonc を更新してデプロイを行います。
# WebアプリとAPIも含む全体デプロイ
pnpm deploy:all- アーキテクチャ解説 (ブログ) - 技術選定の理由と構成図
- デプロイガイド - 本番環境へのデプロイ手順
- デプロイ設計書 - 詳細なシステム設計
- ローカル開発ガイド - Miniflareを使った開発方法
ISC
プロジェクトマネージャー兼開発者