1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zenn記事をInstagram Carousel+Reel動画に自動変換する仕組みを作った

1
Last updated at Posted at 2026-04-03

この記事を読むと何ができるか

Zenn に投稿した技術記事を、Instagram Carousel 画像(PNG × 5〜8枚)と Reel / YouTube Shorts 用の短尺動画(MP4、タイピングアニメーション)にプログラムで自動変換できるようになる。

手動で Canva を開いてスライドを作って……という作業から完全に解放される。記事の構造データを JSON で定義すれば、npx tsx scripts/render-all.ts のワンコマンドで全コンテンツが生成される仕組みだ。


📋 前提条件

  • Node.js 18 以上
  • TypeScript の基本がわかる
  • React の基本がわかる(コンポーネント、props あたり)
  • Instagram / YouTube Shorts への投稿に興味がある

なぜ「記事→SNS」を自動化しようと思ったか

私は製造業で生産技術のエンジニアをしながら、副業で C# コンサルタント事業を準備している。技術記事を Zenn に書いてはいるが、Zenn だけでは認知の広がりに限界を感じていた。

Instagram や YouTube Shorts にもコンテンツを展開したい。でも、記事を書くだけで週5時間がギリギリの稼働時間の中で、Canva を開いてスライドを作って、動画も編集して……という手作業を毎回やるのは現実的じゃない。

「記事の内容を JSON で定義すれば、画像も動画も全部自動で生成される仕組みがほしい」

これが出発点だった。


技術選定 — なぜ Remotion を選んだか

SNS 用の画像・動画を自動生成する方法はいくつかある。私が検討した選択肢と、Remotion を選んだ理由をまとめた。

選択肢 画像生成 動画生成 プログラマブル 学習コスト 判定
Canva ❌ 手作業 ❌ 自動化できない
FFmpeg + ImageMagick 高(コマンドライン地獄) △ 保守性に難
Puppeteer + HTML △ 動画が弱い
Remotion(React) 中(React知識が活きる) ✅ 採用

Remotion を選んだ決め手は3つ:

  1. React でスライドを書ける — HTML/CSS の表現力がそのまま使える。「コードブロックにシンタックスハイライトをつけたい」も React コンポーネントで解決
  2. 静止画(renderStill)も動画(renderMedia)も同じ仕組み — Carousel 画像と Reel 動画を同じプロジェクトで管理できる
  3. GitHub Copilot との相性がいい — React コンポーネントなので、AI にスライドのデザイン調整を依頼しやすい

正直に言うと、Remotion は最初の環境構築で Chrome Headless Shell のダウンロード(約108MB)が走るので「えっ、こんなに重いの?」とは思った。でも2回目以降はキャッシュされるので気にならない。


全体のアーキテクチャ

システムの全体像はシンプルだ:

Zenn記事 (Markdown)
  ↓ AI(Copilot CLI)が分析・要約
ArticleData JSON
  ↓ Remotion レンダリング
├── carousel/slide-01.png 〜 slide-07.png(Carousel 画像)
├── reel/reel-1080x1920.mp4(Reel / Shorts)
├── reel/reel-720x1280.mp4(X/Twitter 用)
└── caption-instagram.txt / caption-x.txt / caption-youtube.txt

ポイントは JSON → 画像/動画 の変換を完全にプログラマブルにした こと。記事の内容が変われば JSON を差し替えるだけで、同じパイプラインからコンテンツが生成される。


Remotion プロジェクトのセットアップ

プロジェクト初期化

# プロジェクトディレクトリを作成
mkdir jodycraft-social && cd jodycraft-social

# 必要なパッケージをインストール
npm init -y
npm install remotion @remotion/bundler @remotion/cli @remotion/renderer react react-dom
npm install -D typescript tsx @types/react @types/react-dom

ディレクトリ構成

jodycraft-social/
├── src/
│   ├── Root.tsx                 # Remotion エントリーポイント
│   ├── types.ts                 # 型定義
│   ├── components/
│   │   ├── carousel/            # Carousel スライドコンポーネント
│   │   │   ├── TitleSlide.tsx
│   │   │   ├── ContentSlide.tsx
│   │   │   ├── CodeSnippetSlide.tsx
│   │   │   ├── SummarySlide.tsx
│   │   │   └── CtaSlide.tsx
│   │   └── reel/                # Reel シーンコンポーネント
│   │       ├── TitleScene.tsx
│   │       ├── CodeTypingScene.tsx
│   │       ├── PointScene.tsx
│   │       └── CtaScene.tsx
│   └── compositions/
│       ├── CarouselSlide.tsx     # Carousel 用 Composition
│       └── CodeReel.tsx          # Reel 用 Composition
├── scripts/
│   ├── render-carousel.ts       # Carousel レンダリングスクリプト
│   ├── render-reel.ts           # Reel レンダリングスクリプト
│   └── render-all.ts            # 一括レンダリングスクリプト
└── out/                         # 生成物の出力先

Instagram Carousel — React コンポーネントで画像を作る

データ構造(CarouselData)

まず、Carousel の入力データの型を定義する:

// src/types.ts
export interface SlideData {
  type: 'title' | 'content' | 'code' | 'summary' | 'cta';
  title?: string;
  subtitle?: string;
  emoji?: string;
  bullets?: string[];
  body?: string;
  code?: string;
  codeLanguage?: string;
  codeCaption?: string;
}

export interface CarouselData {
  articleTitle: string;
  emoji: string;
  topics: string[];
  slides: SlideData[];
  caption: string;
  hashtags: string[];
}

各スライドは type で種類を区別する。Remotion はこの type に基づいて、対応する React コンポーネントをレンダリングする。

コードスニペットスライドの実装例

5種類のスライドの中で、最も「技術記事っぽさ」が出るのがコードスニペットスライド。GitHub Dark テーマ風のデザインにした:

// src/components/carousel/CodeSnippetSlide.tsx
import React from 'react';

export const CodeSnippetSlide: React.FC<{
  title: string;
  code: string;
  codeLanguage: string;
  codeCaption?: string;
}> = ({ title, code, codeLanguage, codeCaption }) => {
  return (
    <div style={{
      width: 1080, height: 1080,
      background: 'linear-gradient(135deg, #0F172A 0%, #1E293B 100%)',
      display: 'flex', flexDirection: 'column',
      padding: 60,
      fontFamily: 'Noto Sans JP, sans-serif',
    }}>
      {/* タイトル */}
      <h2 style={{ color: '#E2E8F0', fontSize: 42, marginBottom: 30 }}>
        {title}
      </h2>

      {/* コードブロック(GitHub Dark風) */}
      <div style={{
        background: '#0D1117',
        borderRadius: 16,
        padding: '30px 35px',
        flex: 1,
        border: '1px solid #30363D',
      }}>
        {/* 言語タグ */}
        <div style={{
          color: '#8B949E', fontSize: 18,
          marginBottom: 20, fontFamily: 'monospace',
        }}>
          {codeLanguage}
        </div>

        {/* コード本体 */}
        <pre style={{
          color: '#E6EDF3', fontSize: 24,
          fontFamily: "'Cascadia Code', 'Fira Code', monospace",
          lineHeight: 1.6, margin: 0, whiteSpace: 'pre-wrap',
        }}>
          {code}
        </pre>
      </div>

      {/* キャプション */}
      {codeCaption && (
        <p style={{ color: '#94A3B8', fontSize: 26, marginTop: 25 }}>
          💡 {codeCaption}
        </p>
      )}
    </div>
  );
};

ここ、ちょっと面白いポイントがあって、Remotion のレンダリングは内部で Chrome Headless Shell を使うので、CSS の表現力がそのまま活きる。linear-gradient でグラデーション背景にしたり、border-radius で角丸にしたり、Web で表現できることはほぼ全部できる。

Composition の定義

Remotion では Composition(コンポジション)を定義して、レンダリング対象を宣言する。Carousel の場合は1枚のスライドが1つの Composition:

// src/compositions/CarouselSlide.tsx
import { Composition, getInputProps } from 'remotion';

export const CarouselSlide: React.FC = () => {
  const props = getInputProps() as { slide: SlideData };
  const { slide } = props;

  switch (slide.type) {
    case 'title':
      return <TitleSlide {...slide} />;
    case 'content':
      return <ContentSlide {...slide} />;
    case 'code':
      return <CodeSnippetSlide {...slide} />;
    case 'summary':
      return <SummarySlide {...slide} />;
    case 'cta':
      return <CtaSlide {...slide} />;
  }
};

レンダリングスクリプト — ハマりポイントあり

ここが一番ハマった。Remotion v4 の renderStill() でスライドごとに異なる inputProps を渡す場合、selectComposition() を毎回呼ぶ必要がある

// scripts/render-carousel.ts(核心部分)
import { bundle } from '@remotion/bundler';
import { renderStill, selectComposition } from '@remotion/renderer';

// 1. バンドルを作成
const serveUrl = await bundle({ entryPoint: './src/index.ts' });

// 2. 各スライドをレンダリング
for (let i = 0; i < slides.length; i++) {
  const inputProps = { slide: slides[i] };

  // ⚠️ ここが重要: スライドごとに selectComposition を呼ぶ
  const composition = await selectComposition({
    serveUrl,
    id: 'CarouselSlide',
    inputProps,  // ← ここで inputProps を渡す
  });

  await renderStill({
    composition,  // selectComposition の結果を使う
    serveUrl,
    output: `out/carousel/slide-${String(i + 1).padStart(2, '0')}.png`,
  });
}

最初にやった間違い: getCompositions() で全 Composition を取得して、renderStill({ composition, inputProps }) で props を渡す方法を試した。しかしこれだと 全スライドが同じ内容(デフォルト props)でレンダリングされてしまう

Remotion v4 では selectComposition()inputProps を渡して、Composition ごとに props を確定させる仕組みに変わっている。公式ドキュメントにも書いてあるが、見落としやすいポイントだった。


Reel 動画 — タイピングアニメーションを作る

Reel / YouTube Shorts 用の縦長動画(1080×1920, 30fps)を生成する。技術記事のコードが1文字ずつタイピングされていくアニメーションは、SNS でけっこう目を引く。

タイピングアニメーションの仕組み

Remotion の useCurrentFrame() を使って、フレーム番号に応じて表示する文字数を計算する。仕組みは単純:

// src/components/reel/CodeTypingScene.tsx
import { useCurrentFrame, useVideoConfig } from 'remotion';

export const CodeTypingScene: React.FC<{
  code: string;
  codeLanguage: string;
  startFrame: number;
}> = ({ code, codeLanguage, startFrame }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const elapsed = frame - startFrame;
  const charsPerFrame = 2; // 30fps で1秒に60文字
  const charsToShow = Math.max(0, Math.floor(elapsed * charsPerFrame));
  const visibleCode = code.slice(0, charsToShow);

  // カーソルの点滅(0.5秒周期)
  const cursorVisible = Math.floor(frame / (fps / 2)) % 2 === 0;

  return (
    <div style={{ /* 背景スタイル省略 */ }}>
      <pre style={{
        color: '#E6EDF3',
        fontFamily: "'Cascadia Code', monospace",
        fontSize: 28,
        lineHeight: 1.5,
      }}>
        {visibleCode}
        {cursorVisible && <span style={{ color: '#58A6FF' }}></span>}
      </pre>
    </div>
  );
};

charsPerFrame = 2 で、30fps なら 1秒に60文字 のタイピング速度になる。コードが 600文字なら約10秒で打ち終わる計算。実際に再生してみて、速すぎたり遅すぎたりしたら調整すればいい。

複数シーンの統合

Reel は「タイトル→コードタイピング→ポイント→CTA」の4シーン構成。各シーンの秒数を durationInSeconds で指定して、フレーム数に変換する:

// 各シーンのフレーム数を計算
const fps = 30;
const scenes = reelData.scenes;
let totalFrames = 0;

for (const scene of scenes) {
  scene._startFrame = totalFrames;
  scene._durationFrames = scene.durationInSeconds * fps;
  totalFrames += scene._durationFrames;
}

// → 例: title(3秒=90f) + code(22秒=660f) + point(6秒=180f) + cta(3秒=90f)
// → 合計 1020フレーム = 34秒

マルチ解像度出力

Instagram Reel と YouTube Shorts は 1080×1920(9:16)だが、X/Twitter は 720×1280 が推奨。renderMedia() で Composition のサイズをオーバーライドすれば、同じコンポーネントから複数解像度を出力できる:

// 1080x1920(Instagram Reel / YouTube Shorts)
const composition1080 = await selectComposition({ serveUrl, id: 'CodeReel', inputProps });
await renderMedia({
  composition: composition1080,
  serveUrl,
  codec: 'h264',
  outputLocation: 'out/reel/reel-1080x1920.mp4',
});

// 720x1280(X/Twitter 用)— Composition サイズをオーバーライド
const composition720 = await selectComposition({ serveUrl, id: 'CodeReel', inputProps });
await renderMedia({
  composition: { ...composition720, width: 720, height: 1280 },
  serveUrl,
  codec: 'h264',
  outputLocation: 'out/reel/reel-720x1280.mp4',
});

一括生成スクリプト — ワンコマンドで全部出す

記事1本から Carousel + Reel + キャプション全部を生成する統合スクリプトを作った:

npx tsx scripts/render-all.ts \
  --input out/visa-article/article-data.json \
  --outDir out/visa-article

これで以下が一発で生成される:

out/visa-article/
├── carousel/
│   ├── slide-01.png 〜 slide-07.png  ← Carousel 画像 7枚
├── reel/
│   ├── reel-1080x1920.mp4           ← Reel / Shorts (34秒, 2.1MB)
│   └── reel-720x1280.mp4            ← X用 (34秒, 2.1MB)
├── caption-instagram.txt             ← ハッシュタグ付きキャプション
├── caption-x.txt                     ← 280文字以内 + リンク
└── caption-youtube.txt               ← タイトル + 説明 + タグ

所要時間はマシンスペックにもよるが、私の環境では 7枚のCarousel + 2本の動画で約2分。初回だけ Chrome Headless Shell のダウンロードに数分かかる。


ハマったところ・意外だったこと

1. ProtocolError: Target closed

レンダリング中に ProtocolError: Target closed というエラーが複数回出た。正直焦ったが、出力ファイルは正常に生成されていた。調べてみると、Remotion が内部的にブラウザタブを開閉する際に発生するもので、レンダリング結果には影響しないとのこと。無視して問題ない。

2. 日本語フォントの問題

Remotion は Chrome Headless Shell でレンダリングするため、システムにインストールされたフォントがそのまま使われる。Windows なら問題ないが、CI/CD 環境(Linux)で動かす場合は Noto Sans JP のインストールが必要になるだろう。

3. コードブロックの改行

JSON の中にコードを書くとき、改行は \n で表現する。ここは当たり前のようで、実際にやってみると「このバックスラッシュ、エスケープ必要だっけ?」と混乱する。TypeScript の JSON ファイルなら素直に \n でOK。

4. Composition の inputProps が反映されない

前述した selectComposition() vs getCompositions() の問題。これは2時間ロスした。Remotion v4 を使うなら selectComposition() + inputProps のパターンを徹底すること。


やってみてわかったこと

「プログラマブルな画像生成」は想像以上に快適

Canva のような GUI ツールだと、「全スライドのフォントサイズを変えたい」とか「背景色を統一したい」という変更が地味に面倒。でも React コンポーネントなら CSS を1箇所変えるだけで全スライドに反映される。

これは 技術記事のように同じフォーマットで量産するコンテンツと相性がいい。記事の内容は毎回変わるが、デザインのフレームは固定。まさにテンプレートの力が活きる場面だ。

AI × Remotion の可能性

今回のパイプラインで一番面白いのは、記事の分析から JSON 生成までを AI(GitHub Copilot CLI)に任せていること。Agent Skill(SKILL.md)に「記事を読んで、スライド構成を JSON で出力して」というルールを書いておくと、Copilot が記事を解析して適切なスライド構成を提案してくれる。

つまり、「記事を書いたら、SNS コンテンツ生成の指示を出すだけ」で全自動化できる。ここの詳細は別記事で書く予定。

動画のファイルサイズが小さい

タイピングアニメーションは画面の変化が少ないため、H.264 の圧縮がよく効く。34秒の Reel が約2MB。Instagram の動画アップロード上限(100MB)を気にする必要はまったくない。


まとめ

  • Remotion は React でプログラマブルに画像・動画を生成できるフレームワーク
  • Instagram Carousel (1080×1080 PNG) も Reel (1080×1920 MP4) も 同じ React プロジェクト で管理できる
  • selectComposition() + inputProps のパターンで、スライドごとに異なるデータを渡す
  • タイピングアニメーションは useCurrentFrame() を使うと簡単に実装できる
  • マルチ解像度出力は Composition のサイズオーバーライドで対応
  • ワンコマンドで Carousel 7枚 + Reel 2本 + キャプション3ファイルを約2分で生成

次の記事では、この仕組みで生成したコンテンツを Buffer GraphQL API を使って Instagram / X / YouTube Shorts に自動投稿するパイプライン について書く予定だ。

📌 この記事が役に立ったら「いいね」してもらえると嬉しいです。SNS運用の自動化シリーズとして記事を書いていく予定なので、フォローもぜひ!


よくある質問

Q. Remotion は無料で使えますか?

Remotion 自体はオープンソース(Business Source License)で、個人利用や小規模プロジェクトなら無料です。企業での大規模利用にはライセンスが必要な場合があります。詳細は Remotion公式のライセンスページ を確認してください。

Q. Canva の方が手軽では?

単発で数枚のスライドを作るなら Canva の方が早いです。でも「毎週の記事からSNSコンテンツを量産する」なら、プログラマブルな Remotion の方が圧倒的に効率がいい。テンプレートを一度作れば、JSON を差し替えるだけで新しいコンテンツが生まれます。

Q. React を知らなくても使えますか?

基本的なコンポーネント(JSX の書き方、props の受け渡し)がわかれば十分です。複雑な状態管理やフックは使わない。CSS の知識の方が重要かもしれません。

Q. CI/CD で自動生成できますか?

技術的には可能です。GitHub Actions で Node.js 環境を用意し、Noto Sans JP フォントをインストールすれば動くはず。ただし Chrome Headless Shell のダウンロードがあるため、初回のビルド時間は長めになります。

Q. 動画にBGMを付けたい場合は?

Remotion は <Audio> コンポーネントで音声の合成もサポートしています。フリーのBGM素材をプロジェクトに配置して、Reel コンポーネントに <Audio src={bgm} /> を追加するだけです。


📝 この記事は Zenn で最初に公開されました。
最新版はZennをご覧ください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?