📽️

[AITuber開発] OBS仮想カメラ+ChatGPT API で 「配信画面をAIが見て自動コメントする機能」を作成する

2025/01/05に公開

[AITuber開発] React+OBS仮想カメラ+ChatGPT (gpt-4o-mini) で 「配信画面をAIが見て自動コメント」する機能をフロントエンドだけで実装する方法

はじめに

最近AITuber OnAirというブラウザだけでAITuber配信のセットアップができるWebアプリをリリースしました。

今はパブリックベータという立ち位置ですが、配信に必要な機能は揃っています。

https://aituberonair.com/

この記事では、私が開発した、この AITuber OnAir という Webアプリの中で実装している機能を題材に、
「OBSの仮想カメラ」から取得した配信画面のスクリーンショットをChatGPTのVision対応モデル(gpt-4o-mini)に投げて、AIアバターにコメントさせる
ための技術的な実装方法を解説します。

具体的な内容

  • サーバーサイド不要(フロントエンドのみ) で完結する仕組み
  • OBSの仮想カメラ で配信中の映像を React アプリから取得し、定期的にスクリーンショットを取得する
  • OpenAI API(Vision対応モデル) に画像 + テキストを送信して、AIによるコメントをストリーミングで受信

あとはAIからのコメントを希望の方針に合わせて実装していくことでやりたいことは実現できると思います。
今回のためにサンプルコードを用意していますが、ほぼほぼAITuber OnAirで実装している内容に近いので、例えばTTSでの処理に向けてAIからレスポンスを分割させている箇所なども実装には入っています。

この機能を実装することで、例えばYouTube Liveなどで AIアバター が自動的に配信画面の状況をコメントしてくれるという面白い体験を実現できます。
しかもそれがフロントエンドだけで完結できるというので、なかなかお手軽にできると思いませんか?

本記事ではサンプルコードを交えながら、React での技術的ポイントを中心に解説していきます。


AITuber OnAir とは?

まず簡単に、私が開発している AITuber OnAir というアプリを紹介しておきます。
AITuber OnAir は「ブラウザだけで AITuber 配信を準備できる」ことを強みとしている Webアプリで、以下のような特徴があります。

  • OBS と連携して、VRMアバター(VTuberモデル)をWeb上で動かしつつ配信
  • YouTube Live のコメント取得・自動返信機能
  • OpenAI API を用いた AIチャット機能や、音声合成エンジン(VOICEVOX / VOICEPEAK / AivisSpeech / OpenAI TTS)との連携
  • Chromeブラウザ だけで配信準備ができ、サーバーサイド構築が不要(音声エンジンはOpenAI TTS以外を動かす場合は別途ローカルで動かす必要あり)

基本的な考えとしてAITuber OnAirはフロントエンドだけで完結するような設計思想になっているため、他にも例えばユーザーが登録したVRMファイルはOPFSを用いてブラウザ内で永続的に管理させるようにするなどの工夫をしています。

AITuber OnAirの使い方については以下のNoteに詳しく書いているのでご覧になってみてください。

https://note.com/aituberonair/n/n39b30eb3eb5b

今回ご紹介する 「配信画面をAIが解析してコメントする」機能 は、この AITuber OnAir の機能の一部を抽出し、サンプルコードとしてまとめたものです。興味がある方はぜひ AITuber OnAir も試してみてください。

※なおソースコードはオープンソースにはしていません。これはビジネス的な要因も今後絡んでくる可能性があるためです。しかしPixivのChatVRMをベースに開発をしており、全体像を把握したいならこちらのソースコードを読むのをおすすめします。また他にもここまで実現できたのはAITuber界隈で先人を切って様々な情報発信されている方々の尽力にほかなりません。そのような人々に倣い、私自身も得ることができた知見が誰かの役に立つのなら、という思いでこちらの記事を書かせていただいています。


機能全体の流れ

それでは本題です。まずは機能全体の流れを説明していきます。

  1. OBSの仮想カメラをON
    • OBS側で仮想カメラを有効化しておきます。
  2. ブラウザで仮想カメラを取得
    • navigator.mediaDevices.getUserMedia({ video: true }) を呼ぶと、Chrome によって PC内蔵カメラかOBS仮想カメラかを選択する画面が表示される...認識なのですが、実はこれがうまく機能しておらず、結局ブラウザ側で設定したカメラがそのまま利用されます。認識誤っていたらコメントで教えていただけると助かります。
    • OBS仮想カメラを選択することで、配信中の画面映像<video> 要素から取得可能になります。
  3. React で <video> 要素を表示しない状態にしつつ裏で再生
    • <video style={{ opacity: 0 }}> のようにしてユーザーには見せない形で映像を流します。
    • 最初はスタイルに display: none などを設定していましたが、これではうまく画像が取得できないので最終的に opacity: 0 に落ち着きました
  4. Canvas を使ってスクショを定期的に取得
    • <video> 要素を drawImage() で Canvas に書き出し、 toDataURL() で Base64 画像を取得
  5. 画像を ChatGPT (gpt-4o-mini) + テキストで送信
    • OpenAI API に対して、Vision対応モデル用に「image_url を含むメッセージ構造」で送る
  6. ストリーミングで返ってくるコメントをリアルタイムに表示・読み上げ(TTS)
    • ここでは文章を一文ずつ切り出して、テキスト表示と音声合成を行う例を示します。

OBSの仮想カメラを有効化する方法

OBSの仮想カメラをONにする方法

まず、OBSの仮想カメラをONにする方法ですが、ここをONにすればそれでOKです。場合によってはドライバーが一緒にインストールされます。

また、今回のサンプルコードを実装すると、ブラウザアクセス時に以下のような通知が表示されます。ここを事前に許可しておく必要があります。

カメラの使用許可

カメラの使用許可 - 2

カメラの使用許可 - 3

またChromeの設定画面からOBSの仮想カメラを選択しておく必要があります。ここまでやることでOBSで仮想カメラを有効化した際に、配信中のOBSの画面をChromeに共有できるようになります。

Google Chromeのカメラ設定でOBSの仮想カメラを選択

以降のコードは上記の設定が済んだ前提で書いていきます。

サンプル実装コード

1. OBS映像を <video> で取得する例

// ObsVideo.tsx
import React, { useEffect, useRef, FC } from 'react';

type ObsVideoProps = {
  onVideoReady?: (videoEl: HTMLVideoElement | null) => void;
};

const ObsVideo: FC<ObsVideoProps> = ({ onVideoReady }) => {
  const videoRef = useRef<HTMLVideoElement>(null);

  useEffect(() => {
    // 1. ブラウザからOBS仮想カメラへアクセス
    navigator.mediaDevices
      .getUserMedia({ video: true })
      .then((stream) => {
        if (videoRef.current) {
          videoRef.current.srcObject = stream;

          // 自動再生は autoPlay 属性でOK
          if (onVideoReady) {
            onVideoReady(videoRef.current);
          }
        }
      })
      .catch((error) => {
        console.error('Error accessing OBS virtual camera:', error);
        if (onVideoReady) {
          onVideoReady(null);
        }
      });
  }, [onVideoReady]);

  return (
    // 画面には映さず、裏で再生しているだけ
    <video
      ref={videoRef}
      autoPlay
      muted
      playsInline
      style={{
        position: 'absolute',
        top: 500,
        width: '100px',
        height: '80px',
        opacity: 0,
        zIndex: 9999,
      }}
      id="obsVideo"
    />
  );
};

export default ObsVideo;

ObsVideo.tsxコンポーネントの意義

  • OBSの映像をReact内に取り込む
    このコンポーネントは、OBSで配信中の画面を仮想カメラとして取得し、Reactアプリ内で <video> 要素に割り当てる役割を持っています。
    その結果、AI向けに画像を送信したり、ブラウザ上でプレビューしたりできるようになります。
  • 映像ストリームの取得をカプセル化
    navigator.mediaDevices.getUserMedia を直接呼び出す処理をひとまとめにすることで、メインのロジックから切り離して保守性を高めるメリットがあります。
  • 画面には表示しないで「裏で再生」
    配信画面をブラウザ上で見たいわけではなく、スクリーンショット取得のために映像ストリームだけ欲しい、というユースケースに対応しています。
  • onVideoReadyコールバック
    親コンポーネントから onVideoReady を受け取り、 <video> 要素が準備できた時点で videoEl を返却することで、別のコンポーネントやロジックから captureScreenshot() などを呼び出せるようにしています。

onVideoReady 関数についてはこのようになっており、ここで取得したvideo要素をrefに格納して処理を行っていきます。

  /**
   * ObsVideoコンポーネントから呼ばれるコールバック
   */
  const handleVideoReady = useCallback((videoElem: HTMLVideoElement | null) => {
    // 取得した<video>要素をrefに格納
    videoEl.current = videoElem;
  }, []);

2. スクリーンショットを取得する captureScreenshot()

上で取得した videoEl に対してのスクリーンショットを取る処理となります。

function captureScreenshot(videoEl: HTMLVideoElement) {
  const canvas = document.createElement('canvas');
  canvas.width = videoEl.videoWidth;
  canvas.height = videoEl.videoHeight;

  const ctx = canvas.getContext('2d');
  if (!ctx) return null;

  ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height);
  return canvas.toDataURL('image/jpeg'); // Base64形式
}

この関数のポイント:

  • Canvas を使ったフレーム取得
    • <video> 要素の現在フレームを canvas に描画し、そこから toDataURL() で静止画をBase64形式として取り出します。
    • これにより、ブラウザだけで実際に描画されているフレームをキャプチャできるので、OBS 仮想カメラの映像から任意タイミングでスクリーンショットを取得できます。
  • 画像サイズの調整
    • canvas.widthcanvas.heightvideoEl.videoWidth / videoEl.videoHeight に合わせることで、動画の解像度に応じた画像を生成 します。
    • 必要に応じて解像度を落とすなどのカスタマイズも可能です。
  • Base64形式をそのまま API 送信用に活用
    • 取得した Data URL (base64) は 後段の OpenAI Visionモデルへの送信 でそのまま利用できます。
    • 画像フォーマットはデフォルトで image/png になりやすいですが、オプションを指定することで image/jpeg などへ変更することができます。

3. OpenAI Visionモデルに送信する関数

次は取得したスクリーンショット画像をOpenAI APIに渡す処理となります。

/**
 * ChatGPTのVision対応モデル (gpt-4o-mini) に
 * 「画像(Base64)+テキスト」 を送信し、
 * ストリーミングで応答を受け取る例
 */
export async function getChatResponseStreamWithVision(
  // 独自の型を用意していますが、実際の中身についてはこのあと出てきます
  messages: VisionMessage[], 
  apiKey: string,
) {
  if (!apiKey) throw new Error('Invalid API Key');

  const headers: Record<string, string> = {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${apiKey}`,
  };

  const requestBody = {
    model: 'gpt-4o-mini',
    messages,
    stream: true,
    max_tokens: 300,
  };

  const res = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers,
    body: JSON.stringify(requestBody),
  });

  if (res.status !== 200 || !res.body) {
    throw new Error('Something went wrong');
  }

  const reader = res.body.getReader();

  // ストリーミング応答をReadableStreamへ変換
  const stream = new ReadableStream({
    async start(controller) {
      const decoder = new TextDecoder('utf-8');

      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          if (!value) continue;

          const data = decoder.decode(value);
          // "data: ..." で区切られているため split
          const lines = data.split('\n');
          for (const line of lines) {
            const trimmed = line.trim();
            if (!trimmed || trimmed === 'data:' || trimmed === 'data: [DONE]') {
              continue;
            }
            if (trimmed.startsWith('data: ')) {
              const jsonStr = trimmed.substring(6);
              try {
                const json = JSON.parse(jsonStr);
                if (json.choices?.[0]?.delta?.content) {
                  controller.enqueue(json.choices[0].delta.content);
                }
              } catch (err) {
                console.error('Parse error chunk:', trimmed);
              }
            }
          }
        }
      } catch (error) {
        controller.error(error);
      } finally {
        reader.releaseLock();
        controller.close();
      }
    },
  });

  return stream;
}

この関数のポイント:

  • Chat Completion API (stream: true) を使用
    • fetch('https://api.openai.com/v1/chat/completions', { ... })stream: true を有効化し、OpenAIからストリーミングでレスポンスを取得します。
    • 大きな応答を小分けで受け取りながら処理したい場合に便利です。
  • Vision対応のメッセージ構造
    • messages 内の最後の contentimage_url を含めることで、画像解析用のプロンプトを送る仕組みになっています。
    • Visionモデル側がこの JSON 構造を読み取り、画像+テキストをもとにした応答を生成します。
  • ストリーミングレスポンスの処理
    • OpenAIのレスポンスは「data: {JSON}」という形で数行(場合によっては多数)に分割されて返ってきます。
    • decoder.decode(value).split('\n') で行ごとにパースし、json.choices[0].delta?.content があればそれを controller.enqueue() で外部に渡しています。
  • ReadableStream で再構築
    • こうしたバイトストリームを ブラウザ標準の Stream APIReadableStream)に変換することで、React アプリ側で使い回ししやすい形にしています。

4. AIコメントを生成するメインロジック

/**
 * setInterval などで1分おきに呼び出している想定です
 */
const handleAutoVisionComment = useCallback(async () => {
  if (!openAiKey) {
    console.warn('APIキーが入力されていません');
    return;
  }

  // スクリーンショット取得
  const dataUrl = captureScreenshot(videoEl);
  if (!dataUrl) {
    console.warn('スクリーンショット取得失敗');
    return;
  }

  const base64Image = dataUrl.split(',')[1] || '';

  // 過去のチャットログ (chatLog) と、Visionの専用system Prompt
  const messages: Message[] = [
    { role: 'system', content: visionSystemPrompt },
    ...chatLog,
  ];

  // Vision対応メッセージ形式
  const messagesForVision: VisionMessage[] = [
    ...messages,
    {
      role: 'user',
      content: [
        {
          type: 'text',
          text: '配信画面をチェックしてコメントしてね。',
        },
        {
          type: 'image_url',
          image_url: {
            url: `data:image/jpeg;base64,${base64Image}`,
            detail: 'low',
          },
        },
      ],
    },
  ];

  // OpenAIに問い合わせ
  const stream = await getChatResponseStreamWithVision(messagesForVision, openAiKey).catch(
    (err) => {
      console.error(err);
      return null;
    }
  );
  if (!stream) {
    return;
  }

  // ストリーミングでテキストを少しずつ受け取り処理
  const reader = stream.getReader();
  let receivedMessage = '';
  const sentences = [];

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      // 次々と追記していく
      receivedMessage += value;

      // 例:句点や改行で一文ずつ切り出す
      const sentenceMatch = receivedMessage.match(/^(.+[。.!?\n]|.{10,}[、,])/);
      if (sentenceMatch && sentenceMatch[0]) {
        const sentence = sentenceMatch[0];
        sentences.push(sentence);
        receivedMessage = receivedMessage.slice(sentence.length).trimStart();

        // UI 表示や音声合成に繋げる処理を以降で書く
      }
    }
  } catch (e) {
    console.error(e);
  } finally {
    reader.releaseLock();
  }

  // ここでは「配信画面を見た」ログは残さない例
}, [openAiKey, chatLog, visionSystemPrompt, videoEl]);

このメインロジックのポイント:

  • 定期的にスクリーンショット + API呼び出し
    • たとえば setIntervalsetTimeout などで一定周期(30秒〜1分など)ごとにこの関数を呼ぶ設計にすることで、配信画面を随時チェックし、AIコメントを生成させられます。
  • スクリーンショットを取得してBase64を抽出
    • captureScreenshot(videoEl)dataUrl というフローを経て、dataUrl.split(',')[1] を取得することで 純粋なBase64文字列 のみを抽出します。
  • Vision対応のメッセージを組み立て
    • content フィールドに { type: 'image_url', image_url: {...} } を含めることで、Visionモデルが画像解析を行うことができます。
  • ストリーミングされた文章を逐次切り出す
    • receivedMessage += value;match(/^(.+[。.!?\n]|.{10,}[、,])/) のロジックで、句点や改行などを区切りに一文ずつ取り出しています。
    • 取り出した文は画面表示に反映したり、音声合成(TTS)の入力として利用できます

以上がそれぞれの処理ブロックにおけるポイントです。

これらを組み合わせることで、OBS仮想カメラ経由で取得した配信画面をAIに解析させ、コメントを返させる機能がフロントエンド(React)だけで実現可能となります。


実装上の注意点

  1. セキュリティ / レート制限

    • ブラウザ側に直接 OpenAI APIキー を埋める形になるので、無制限に公開すると不正利用されるリスクがあるので注意してください。
    • なお、 AITuber OnAir ではOpenAI APIキーはユーザーが取得し登録するものとなっており、データはlocalStorageにのみ保存される仕様としています。
  2. OBS 仮想カメラの設定

    • Chrome 側の「カメラ選択ダイアログ」で確実に「OBS Virtual Camera」を選択してください。
    • 物理カメラが選ばれると、配信画面ではなく自分の顔が送信されてしまう可能性があるため注意が必要です。
  3. 画像サイズ / ネットワーク負荷

    • フルHDなど巨大な画像を高頻度で送ると、トークン数・帯域ともに大きく消費します。
    • canvas に描画する際にサイズを落としたり、解像度を下げた JPEG に変換したりすると負荷を低減できます。

まとめ

  • OBS 仮想カメラ + React + OpenAI Vision を組み合わせると、配信画面をAIに解析させ、コメントを生成 できる。
  • サーバーレス構成(ブラウザだけ)でも動作させられるため、手軽に開発・運用可能。
  • AITuber配信で画面の状況を逐一説明してくれたり、視聴者のコメントだけでなく 画面上の変化 にも反応してくれる面白い仕掛けが作れる。

ブラウザだけでも結構色々できる

先にも書いた通り、この仕組みは私が作っている AITuber OnAir というWebアプリで採用しており、

  • YouTube Live のコメント読み上げ
  • AI の自動トーク
  • VRMアバターでの表情連動
  • 配信画面の解析
    … といった要素を ブラウザのみ で完結させています。

AITuber配信 にチャレンジしたい」「ChatGPTgpt-4o-mini の活用例を見たい」「サーバー無し でどこまでできるのか気になる」といった方は、ぜひ参考にしてみてください。

それでは!

Discussion