キカガク プラットフォームブログ

株式会社キカガクのプラットフォームブログです。エンジニアやデザイナー、プロダクトマネージャーなどが記事を書いています。

Next.js × OpenAI API × VOICEVOX で喋る AI を作ってみた

はじめに

こんにちは、株式会社キカガク、プラットフォーム部の西村です。 みなさんは AI を好きな声で喋らせたいと思ったことはありませんか? 今回は Next.js と OpenAI API、そして VOICEVOX を使って、喋る AI を作成する方法を紹介します。 なお、この記事ではローカルサーバーで動かすことまでを目指します。(公開は想定していません)

必要な事前知識

  • Next.js (App Router) の基本知識
  • OpenAI の API の基本知識

完成像

テキストエリアにテキストを入力して、音声生成ボタンをクリックすると、音声が再生されます。

ざっくり説明すると、以下のような流れで実装しています。

  1. 文字を入力
  2. OpenAI API を利用し AI の回答を取得
  3. VOICEVOX API を利用して音声を合成
  4. 出力

ここから順に説明していきます。

手順

Next.js のセットアップ

create-next-app でプロジェクトを作成してください。細かい説明は割愛します。

本記事は バージョン 14.2.5、App Router で実装しています。

OpenAI の API を叩くセットアップ

必要なパッケージをインストールします。

npm install openai
#or
yarn add openai

次に、API キーを.env.localファイルに保存します。 API キー を取得する方法は省略します!使ったことない方は調べてみてください!

# .env.local
OPENAI_API_KEY=your-openai-api-key

次に、API エンドポイントを作成します。このエンドポイントは、ユーザーからのメッセージを OpenAI のチャットモデルに送信し、返信を取得します。 App Router を使ったことがない方は、公式のドキュメントを確認してイメージを掴んでいただければと思います。

// src/app/api/openai/route.ts
import { NextResponse } from "next/server";
import OpenAI from "openai";

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

export async function POST(request: Request) {
  const { message } = await request.json();

  if (!message) {
    return NextResponse.json(
      { message: "Message is required" },
      { status: 400 }
    );
  }

  try {
    const chatCompletion = await openai.chat.completions.create({
      model: "gpt-4",
      messages: [
        {
          role: "user",
          content: message,
        },
      ],
    });

    return NextResponse.json(
      {
        result: chatCompletion.choices[0].message.content,
      },
      { status: 200 }
    );
  } catch (error) {
    return NextResponse.json(
      {
        error: error instanceof Error ? error.message : "エラーが発生しました",
      },
      { status: 500 }
    );
  }
}

このコードでは、ユーザーからの POST リクエストを受け取り、メッセージが含まれているかを確認します。含まれていない場合はエラーレスポンスを返します。メッセージがある場合は、OpenAI の API を呼び出してチャットの返信を取得し、その結果を JSON 形式で返します。

API クライアントを使用して、機能するか確認します。(ここでは Thunder Client を使用します)

Thunder Client を使用して挙動確認をしているスクリーンショット

問題なく返信が取得できていますね!これで Open AI の API を使う準備は完了です。

VOICEVOX のセットアップ

VOICEVOX とは、無料で使える中品質なテキスト読み上げ・歌声合成ソフトウェアです。VOICEVOX を使用して実現したいことは以下の 2 つです。

1. テキストからクエリを作成する。

2. 作成したクエリを基に合成音声を作成する

まずはVOICEVOX の公式サイトから VOICEVOX 本体をダウンロードしてください。

VOICEVOX は API を提供しています。アプリの起動後に以下の URL にアクセスするとドキュメントを確認できます。

http://127.0.0.1:50021/docs

早速、Thunder Client を使って確認してみましょう。

1. テキストからクエリを作成する

クエリとは、音声合成するために必要な情報で、セリフ以外にもアクセントなどの設定が含まれます。 もしアクセントを修正したい場合は、このクエリを編集してチューニングします。 例えば、http://127.0.0.1:50021/audio_query?text=あいうえお&speaker=1 で POST すると以下のようなクエリが取得できます。

{
  "accent_phrases": [
    {
      "moras": [
        {
          "text": "",
          "consonant": null,
          "consonant_length": null,
          "vowel": "a",
          "vowel_length": 0.27477818727493286,
          "pitch": 5.669236183166504
        },
        // 略

text で内容を、speaker でキャラクターとスタイルを選択しています。 speaker の id について、誰が何に該当するかは http://127.0.0.1:50021/speakers で確認することができます。 (speaker=1 は「ずんだもん」の「あまあま」スタイルになります。)

2. クエリに沿って音声合成する

body に先ほど取得したクエリをコピペして、http://127.0.0.1:50021/synthesis?speaker=1 に送信してみましょう。成功した場合、レスポンスとして音声のバイナリが返ってくるので、ファイルとして保存します(Thunder Client の場合は「Save File」) 。この時点で音楽再生ソフトで再生できるハズです。 Thunder Client を使用して挙動確認をしているスクリーンショット

実装

次に、これらを使用して、テキストから音声合成を行うエンドポイントを作成します。

// src/app/api/voicevox/route.ts~

import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const { text } = await request.json();

  if (!text) {
    return NextResponse.json({ message: "Text is required" }, { status: 400 });
  }

  try {
    // 音声合成用のクエリを作成
    const queryResponse = await fetch(
      `http://127.0.0.1:50021/audio_query?text=${encodeURIComponent(
        text
      )}&speaker=1`,
      {
        method: "POST",
      }
    );

    if (!queryResponse.ok) {
      const errorData = await queryResponse.text();
      throw new Error(`Failed to create audio query: ${errorData}`);
    }

    const queryData = await queryResponse.json();

    // 音声合成
    const synthesisResponse = await fetch(
      "http://127.0.0.1:50021/synthesis?speaker=1",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(queryData),
      }
    );

    if (!synthesisResponse.ok) {
      const errorData = await synthesisResponse.text();
      throw new Error(`Failed to synthesize audio: ${errorData}`);
    }

    const audioBlob = await synthesisResponse.blob();

    return new Response(audioBlob, {
      status: 200,
      headers: {
        "Content-Type": "audio/wav",
      },
    });
  } catch (error: unknown) {
    return NextResponse.json(
      {
        error: error instanceof Error ? error.message : "Something went wrong",
      },
      { status: 500 }
    );
  }
}

このコードは、POST リクエストで受け取ったテキストを元に音声合成用のクエリを作成し、そのクエリを使って音声を合成します。生成された音声は音声ファイルとしてレスポンスに含められます。

最後に、Thunder Client で挙動を確認してみましょう。Save File で音声ファイルを保存し、プレイヤーで問題なく再生できることを確認してください。body に入れた text の内容で音声ファイルが作成できれば成功です。 Thunder Client を使用して挙動確認をしているスクリーンショット

注意点

利用規約は遵守してください。規約はキャラクター毎に異なるので必ずチェックしてください。

クライアント側で実行

では、ここまで実装した内容を、クライアント側で実行してみましょう。ユーザーが入力したテキストを OpenAI API に送り、その結果を VOICEVOX API で音声に変換する UI を作成します。

※スタイルは適当です。

// src/app/page.tsx

"use client";

import React, { useState, useRef, useEffect } from "react";

export default function Page() {
  const [message, setMessage] = useState<string>("");
  const [generatedMessage, setGeneratedMessage] = useState<string>("");
  const [audioUrl, setAudioUrl] = useState<string>("");
  const audioRef = useRef<HTMLAudioElement>(null);

  const handleGenerateVoice = async () => {
    if (!message) {
      alert("プロンプトを入力してください");
      return;
    }

    try {
      // OpenAI APIを呼び出してメッセージを生成
      const openaiResponse = await fetch("/api/openai", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ message }),
      });

      if (!openaiResponse.ok) {
        const errorText = await openaiResponse.text();
        alert("OpenAI APIの呼び出しに失敗しました。");
        return;
      }

      const openaiData: { result: string } = await openaiResponse.json();
      setGeneratedMessage(openaiData.result);

      // VOICEVOX APIで音声を生成
      const voicevoxResponse = await fetch("/api/voicevox", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ text: openaiData.result }),
      });

      if (!voicevoxResponse.ok) {
        const errorText = await voicevoxResponse.text();
        alert(`VOICEVOX APIの呼び出しに失敗しました。${errorText}`);
        return;
      }

      const voicevoxBlob = await voicevoxResponse.blob();
      const voicevoxUrl = URL.createObjectURL(voicevoxBlob);
      setAudioUrl(voicevoxUrl);
    } catch (error) {
      alert("API呼び出し中にエラーが発生しました。");
    }
  };

  useEffect(() => {
    if (audioRef.current && audioUrl) {
      audioRef.current.play();
    }
  }, [audioUrl]);

  return (
    <div style={{ padding: "20px" }}>
      <h1 style={{ fontSize: "2em", color: "white", textAlign: "center" }}>
        おしゃべりAI (VOICEVOX:ずんだもん)
      </h1>

      <textarea
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        placeholder="プロンプトを入力してください"
        style={{ width: "100%", height: "100px", marginBottom: "20px" }}
      />

      <button
        onClick={handleGenerateVoice}
        style={{
          display: "block",
          width: "100%",
          padding: "10px",
          backgroundColor: "#FF0080",
          color: "white",
          border: "none",
          borderRadius: "5px",
          cursor: "pointer",
        }}
      >
        音声生成
      </button>

      {generatedMessage && (
        <div style={{ marginTop: "20px" }}>
          <h2 style={{ fontSize: "1.5em", color: "white" }}>
            生成されたメッセージ:
          </h2>
          <p>{generatedMessage}</p>
        </div>
      )}

      {audioUrl && (
        <div style={{ marginTop: "20px" }}>
          <h2 style={{ fontSize: "1.5em", color: "white" }}>生成された音声:</h2>
          <audio
            ref={audioRef}
            src={audioUrl}
            controls
            style={{ width: "100%" }}
          />
        </div>
      )}
    </div>
  );
}

このコードは、ユーザーが入力したテキストを基に OpenAI の API と VOICEVOX の API を呼び出し、生成されたメッセージを音声として再生します。useEffect を使って音声が生成された後に自動的に再生されるようにしています。

テキストエリアにテキストを入力して、音声生成ボタンをクリックすると、音声が再生されます! これで喋る AI の実装は完了です!

さいごに

いかがでしたか?今回は、Next.js、OpenAI API、そして VOICEVOX を使って喋る AI を作成する方法をご紹介しました。 アイデア次第で様々な用途に応用できるので、ぜひ、自分なりの工夫を加えて開発してみてください!

余談ですが、私は Speech-to-Textを使って、会話ができるようにしてみました!音声認識の精度も高く、問題なく会話ができました。機会があれば記事にしようと思います!