AIコーディング 2026.07.06

ChatGPT APIでAI音声フィーディング実装|リアルタイムストリーミング完全ガイド【2026年版】

タグ:ChatGPT API / 音声ストリーミング / リアルタイム実装 / Voice Mode / Web Audio API

ChatGPT APIで音声ストリーミングを実装する意味

ChatGPT Voice Modeが一般化し、テキストだけでなく音声でAIと対話したいというニーズが急速に高まっています。従来のテキストベースのAPIとは異なり、リアルタイムで音声をストリーミング送受信する実装は、開発者にとって新しいスキルセットとなっています。

このガイドでは、ChatGPT APIを使ってリアルタイム音声フィーディング(音声の入出力)を実装する方法を、実行可能なコード例とともに解説します。完成させれば、ユーザーが話しかけた音声をリアルタイムでAIが理解し、音声で応答するアプリケーションが作成できます。

前提条件と必要な環境

必要なもの

  • Node.js 18.0以上(サーバーサイド処理用)
  • OpenAI APIキー(ChatGPT APIの利用権)
  • ブラウザのWeb Audio API対応(音声キャプチャ用)
  • マイク・スピーカーの接続(物理的な音声デバイス)

パッケージのインストール

リアルタイム音声ストリーミングを実装するには、以下のパッケージが必要になります。

npm install openai dotenv express
npm install --save-dev typescript ts-node @types/node

環境変数の設定

APIキーを安全に管理するため、.envファイルを作成してください。

# .env
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PORT=3000

ステップ1: サーバーサイドの音声受信エンドポイント構築

ChatGPT APIへの音声ストリーミングをハンドルするバックエンド処理を実装します。Express.jsを使い、クライアントから送信された音声バイナリデータを受け取るエンドポイントを構築します。

// server.js
const express = require('express');
const { OpenAI } = require('openai');
require('dotenv').config();

const app = express();
app.use(express.json());
app.use(express.raw({ type: 'audio/wav', limit: '50mb' }));

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// 音声をテキストに変換し、ChatGPTで処理するエンドポイント
app.post('/api/voice-chat', async (req, res) => {
  try {
    // クライアントから送信された音声バイナリデータを取得
    const audioBuffer = req.body;

    // Whisper APIを使用して音声をテキストに変換
    const transcription = await openai.audio.transcriptions.create({
      file: new File([audioBuffer], 'audio.wav', { type: 'audio/wav' }),
      model: 'whisper-1',
      language: 'ja', // 日本語指定
    });

    const userMessage = transcription.text;
    console.log('ユーザーの音声認識結果:', userMessage);

    // ChatGPTにテキストを送信
    const chatResponse = await openai.chat.completions.create({
      model: 'gpt-4',
      messages: [
        {
          role: 'system',
          content: 'You are a helpful voice assistant. Respond concisely in Japanese.',
        },
        {
          role: 'user',
          content: userMessage,
        },
      ],
    });

    const responseText = chatResponse.choices[0].message.content;
    console.log('ChatGPTの応答:', responseText);

    // テキストを音声に変換
    const speechResponse = await openai.audio.speech.create({
      model: 'tts-1',
      voice: 'alloy',
      input: responseText,
    });

    // 音声バイナリデータをクライアントに送信
    const audioBuffer2 = await speechResponse.arrayBuffer();
    res.setHeader('Content-Type', 'audio/wav');
    res.send(Buffer.from(audioBuffer2));
  } catch (error) {
    console.error('エラー:', error);
    res.status(500).json({ error: error.message });
  }
});

app.listen(process.env.PORT || 3000, () => {
  console.log(`サーバーが http://localhost:${process.env.PORT || 3000} で起動しました`);
});

このエンドポイントは以下の流れで動作します:

  1. クライアントから音声バイナリデータ(WAV形式)を受け取る
  2. Whisper APIでそれを日本語テキストに変換
  3. ChatGPT(gpt-4モデル)にテキストを送信して応答を取得
  4. Text-to-Speech APIで応答テキストを音声に変換
  5. 生成された音声をクライアントに返送

ステップ2: クライアント側の音声キャプチャ実装

ブラウザのWeb Audio APIを使用して、ユーザーのマイク入力をキャプチャし、サーバーに送信する処理を実装します。

<!-- index.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>AI音声アシスタント</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      max-width: 600px;
      margin: 50px auto;
      padding: 20px;
    }
    button {
      padding: 12px 24px;
      font-size: 16px;
      cursor: pointer;
      border: none;
      border-radius: 4px;
      background: #0066cc;
      color: white;
    }
    button:hover {
      background: #0052a3;
    }
    button:disabled {
      background: #ccc;
      cursor: not-allowed;
    }
    #status {
      margin-top: 20px;
      padding: 10px;
      border-radius: 4px;
      display: none;
    }
    #status.recording {
      display: block;
      background: #fff3cd;
      color: #856404;
    }
    #status.processing {
      display: block;
      background: #cfe2ff;
      color: #084298;
    }
    #status.error {
      display: block;
      background: #f8d7da;
      color: #842029;
    }
    #transcript {
      margin-top: 20px;
      padding: 10px;
      background: #f5f5f5;
      border-radius: 4px;
      min-height: 40px;
    }
  </style>
</head>
<body>
  <h1>AI音声アシスタント</h1>
  <button id="recordBtn">音声録音開始</button>
  <button id="stopBtn" disabled>停止</button>
  <div id="status"></div>
  <div id="transcript">会話内容がここに表示されます</div>

  <script>
    let mediaRecorder;
    let audioChunks = [];

    const recordBtn = document.getElementById('recordBtn');
    const stopBtn = document.getElementById('stopBtn');
    const statusDiv = document.getElementById('status');
    const transcriptDiv = document.getElementById('transcript');

    // 音声録音開始
    recordBtn.addEventListener('click', async () => {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
        audioChunks = [];

        mediaRecorder.addEventListener('dataavailable', (event) => {
          audioChunks.push(event.data);
        });

        mediaRecorder.addEventListener('stop', async () => {
          // 音声をWAV形式に変換して送信
          const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
          await sendAudioToServer(audioBlob);
        });

        mediaRecorder.start();
        recordBtn.disabled = true;
        stopBtn.disabled = false;

        updateStatus('音声を録音中...', 'recording');
      } catch (error) {
        updateStatus(`マイクアクセスエラー: ${error.message}`, 'error');
      }
    });

    // 音声録音停止
    stopBtn.addEventListener('click', () => {
      mediaRecorder.stop();
      recordBtn.disabled = false;
      stopBtn.disabled = true;
      updateStatus('', '');
    });

    // サーバーに音声を送信
    async function sendAudioToServer(audioBlob) {
      try {
        updateStatus('サーバーで処理中...', 'processing');

        const response = await fetch('/api/voice-chat', {
          method: 'POST',
          headers: { 'Content-Type': 'audio/webm' },
          body: audioBlob,
        });

        if (!response.ok) {
          throw new Error(`サーバーエラー: ${response.status}`);
        }

        // サーバーから返された音声を再生
        const responseAudio = await response.arrayBuffer();
        playAudio(responseAudio);
        updateStatus('', '');
      } catch (error) {
        updateStatus(`送信エラー: ${error.message}`, 'error');
      }
    }

    // 音声を再生
    function playAudio(audioBuffer) {
      const audioContext = new (window.AudioContext || window.webkitAudioContext)();
      audioContext.decodeAudioData(audioBuffer, (buffer) => {
        const source = audioContext.createBufferSource();
        source.buffer = buffer;
        source.connect(audioContext.destination);
        source.start(0);
      });
    }

    // ステータスメッセージを更新
    function updateStatus(message, className) {
      statusDiv.textContent = message;
      statusDiv.className = className;
    }
  </script>
</body>
</html>

このクライアント実装のポイント:

  • getUserMedia APIを使ってマイクのアクセスを要求
  • MediaRecorder APIで音声ストリーミングを録音
  • 音声データをサーバーに送信して処理
  • 返ってきた音声データをAudioContextで再生

ステップ3: リアルタイムストリーミングの最適化

上記の基本実装では、音声を録音完了してからサーバーに送信する「バッチ処理」になっています。真の「リアルタイムストリーミング」を実現するには、録音中に音声チャンクを継続的にサーバーに送信する必要があります。

// server-streaming.js(改良版)
const express = require('express');
const { OpenAI } = require('openai');
require('dotenv').config();

const app = express();
app.use(express.raw({ type: 'audio/wav', limit: '50mb' }));

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// リアルタイムストリーミング用エンドポイント
app.post('/api/voice-stream', async (req, res) => {
  try {
    const audioBuffer = req.body;

    // ストリーミング応答を開始
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    // 音声を認識(ストリーミング)
    const transcription = await openai.audio.transcriptions.create({
      file: new File([audioBuffer], 'audio.wav', { type: 'audio/wav' }),
      model: 'whisper-1',
    });

    // ChatGPTに送信(ストリーミング応答)
    const stream = await openai.chat.completions.create({
      model: 'gpt-4',
      messages: [
        {
          role: 'user',
          content: transcription.text,
        },
      ],
      stream: true,
    });

    // チャンクごとにクライアントに送信
    for await (const chunk of stream) {
      if (chunk.choices[0].delta.content) {
        res.write(`data: ${JSON.stringify({ text: chunk.choices[0].delta.content })}\n\n`);
      }
    }

    res.end();
  } catch (error) {
    console.error('ストリーミングエラー:', error);
    res.status(500).json({ error: error.message });
  }
});

app.listen(process.env.PORT || 3000, () => {
  console.log(`ストリーミングサーバーが起動しました`);
});

このアプローチでは、ChatGPTの応答をストリーミングで受け取り、テキストが確定する前にクライアントに送信できるため、ユーザー体験が大幅に向上します。

よくあるエラーと対処法

エラー1: 「Whisper APIが音声ファイルを認識できない」

症状

Error: Invalid file format. Supported formats: mp3, mp4, mpeg, mpga, m4a, ogg, opus, flac, wav

原因 Web Audio APIがキャプチャしたaudio/webm形式をそのままWhisper APIに送信している。Whisper APIは特定の形式のみをサポートしています。

対処法 クライアント側でWebMをWAV形式に変換してから送信するか、サーバー側でFFmpegを使って変換してください。

// クライアント側: WebM → WAV変換
async function convertWebMToWav(webmBlob) {
  const arrayBuffer = await webmBlob.arrayBuffer();
  const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
  
  // AudioBufferからWAV形式に変換(ライブラリを使用)
  // 例: audiobuffer-to-wav パッケージを使うと簡単
  return audioBufferToWav(audioBuffer);
}

エラー2: 「CORS エラーが発生している」

症状

Access to XMLHttpRequest at 'http://localhost:3000/api/voice-chat' from origin 'http://localhost:8000' has been blocked by CORS policy

原因 クライアントとサーバーが異なるオリジン(ポート番号など)で動作しており、ブラウザのCORS保護により通信がブロックされています。

対処法 Express.jsサーバーにCORS設定を追加してください。

const cors = require('cors');

app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true,
}));

エラー3: 「マイクアクセス権限がない」

症状

NotAllowedError: Permission denied

原因 ユーザーがブラウザのマイクアクセスを拒否している、またはHTTPS環境でないため。

対処法 本番環境ではHTTPSを使用し、ユーザーに許可を再度求めるようUI設計をしてください。

// エラーハンドリング
navigator.mediaDevices.getUserMedia({ audio: true })
  .catch(error => {
    if (error.name === 'NotAllowedError') {
      console.log('ユーザーが許可を拒否しました');
    } else if (error.name === 'NotFoundError') {
      console.log('マイクが見つかりません');
    }
  });

応用:複数ユーザーでの同時音声チャット

上記の基本実装を拡張することで、複数ユーザーがリアルタイムで音声チャットできるシステムも構築できます。WebSocketを使ってサーバーとクライアント間の双方向通信を実現し、チャット相手の音声をストリーミング配信すれば、遅延を最小化した音声会話アプリケーションが完成します。

また、OpenAI APIのモデルをカスタマイズすることで、特定の業界(医療、カスタマーサポート、教育など)向けの音声アシスタントも開発可能です。

次のステップ

  • 長時間会話への対応:セッション管理を実装し、複数ターンの会話履歴を保持
  • 音声の品質向上:ノイズキャンセレーション機能の追加
  • 多言語サポート:Whisper APIの言語パラメータを動的に変更
  • キャッシング戦略:同じ質問への応答をキャッシュして応答速度を改善

あわせて読みたい

参考ソース