TL;DR
音声でGPTと会話するアプリを、node.jsで作ってみました。
音声データはSocket.ioでサーバーと通信し、テキストとの変換にはGoogleのAPIを使っています。
┌───────┐ ┌─────────┐ ┌──────────────┐
│ │localhost:3000│ Client │ │Google │
│Browser├──────────────┤ │ ┌───┤Text to Speech│
│ │ html,js,css│React App│ │ │Speech to Text│
└────┬──┘ └─────────┘ │ └──────────────┘
│ │
│ ┌─────────┐ │ ┌──────────────┐
│ localhost:3001│ Server │ │ │OpenAI │
└─────────────────┤ ├───┴───┤ │
Audio Data│ Express │ │gpt-3.5 │
└─────────┘ └──────────────┘
最終的なコードは以下にあります。実行方法はREADMEを参照してください。
各技術要素の説明
各技術要素について、簡単に説明していきます。
Socket.ioを利用したリアルタイム通信
サーバーとクライアント間でリアルタイム通信を実現するために、Socket.ioを使用しています。Socket.ioは、WebSocketをベースにしていますが、WebSocketが利用できない環境ではロングポーリングなどの別の通信方式を利用できます。
サーバー側のhttpServer.listen()
までの処理で、HTTPサーバーを立ち上げています。
クライアントとの通信が確立できた後の処理は、io.on("connection",...)
のハンドラ内で行います。
const PORT = process.env.PORT || 3001;
const httpServer = createServer();
const io = new Server(httpServer, {
cors: {
origin: "*", // Allow all origins
methods: ["GET", "POST"],
},
});
httpServer.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
io.on("connection", (socket) => {
console.log("Client connected.");
// ...
});
クライアント側ではio()
でサーバーとの接続を行います。
データ送信は.emit()
を使い、データ受信時の処理は.on()
に記載します。
Reactの再レンダリングでio
が再初期化されたりしないように、useRef
を使って状態を保持しています。
import { io } from "socket.io-client";
const SERVER_URL = "http://localhost:3001";
const socketRef = useRef();
socketRef.current = io(SERVER_URL);
音声の録音とストリーミング送信
navigator.mediaDevices.getUserMedia
を使用して、ユーザーのマイクからデータを取得します。
音声データの転送にはMediaRecorder
を使い、定期的に(ストリーミングで)サーバーへ送信するため、start()
の引数にタイムスライスを渡しています。これによって、この場合では100ms時間ごとに、音声データをサーバーへ転送します。
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
audioRef.current = new MediaRecorder(stream);
audioRef.current.ondataavailable = (e) => {
if (e.data.size > 0) {
socketRef.current.emit("clientAudio", e.data);
}
};
audioRef.current.start(100);
サーバーから受け取った音声の再生
クライアントは、サーバーから送られてくる音声データ(GPTによって生成されたテキストを音声合成したもの)を受信し、再生します。AudioContextを使用して音声データをデコードし、audioContext.destination
に対して出力することで音声を再生します。
socketRef.current.on('serverAudio', async (data) => {
const audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(data);
const bufferSource = audioContext.createBufferSource();
bufferSource.buffer = audioBuffer;
bufferSource.connect(audioContext.destination);
bufferSource.start();
});
サーバーでのAPI呼び出し
GoogleのAPIを使って音声とテキストの変換をする部分や、OpenAIのAPIで回答を得る部分については、呼び出している以上の内容がないため、説明は省略します。
以上。