LoginSignup
90
104

【ハンズオン】ReactとGPT-4oでキャラと話せる音声チャットを作ろう【TypeScript/TailwindCSS】

Last updated at Posted at 2024-05-19

はじめに

GPT-4oのリリースで大きく世界が変わった。時代についていくにはAIを使いこなす必要がある

GPT-4oの登場は過去のGPTの中でも特に印象に残っています。とにかく返答が早く、スマホアプリでの音声チャットはほぼ人と話しているのと感覚が変わりませんでした。

今後AI技術を活かしながら新しいサービスを作ることはどんどん広がり、それがビジネスにつながっていくとはずです。AIを使ってどのような価値が作れるのかという視点をもてることが重要です

AIを使って何ができるのかを思いつくには、AIを試してみて何を実現することができるのかを理解していないといけません。

今回はChatGPTの音声入力がとても印象的だったので、それを真似てキャラクターとお話ができるリアルタイム音声チャットアプリをReactを用いてハンズオンで作成していきます。

動画での解説

細かいところなどは動画でより詳しく解説していますので、合わせてご利用ください!

1.開発環境の用意

まずはReactとTailwindCSSの環境を用意します。
ここではNode環境があることを前提にして行います。

$ npm create vite@latest
✔ Project name: … voice-chat-qiita
✔ Select a framework: › React
✔ Select a variant: › TypeScript
$ cd voice-chat-qiita
$ npm i
$ npm run dev

localhost:5173を開きます

image.png

Reactの環境を作成することができました。続いてTailwindCSSを導入します。

$ npm install -D tailwindcss postcss autoprefixer
$ npx tailwindcss init -p

VSCodeで作成したプロジェクトを開きます。

tailwind.config.jsを以下にします

tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

src/index.cssを以下にします

src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

tailwindが正しく反映されているかを確認します。
src/app.tsxを以下に変更します

src/App.tsx
import "./App.css";

function App() {
  return (
    <>
      <h1 className="text-3xl font-bold underline">Hello world!</h1>
    </>
  );
}

export default App;

サーバーを再起動して確認するとスタイルがあたっているので導入がうまくいきました

image.png

2. 音声入力の実装

音声入力をして入力したテキストを表示する実装をします
今回はreact-speech-recognitionというライブラリを使います。

$ npm i react-speech-recognition
$ npm i --save-dev @types/react-speech-recognition
$ npm i regenerator-runtime

App.tsxを以下に変更します。

src/App.tsx
import "regenerator-runtime";
import SpeechRecognition, {
  useSpeechRecognition,
} from "react-speech-recognition";
import { useState } from "react";

function App() {
  const {
    listening,
    transcript,
    resetTranscript,
    browserSupportsSpeechRecognition,
  } = useSpeechRecognition();
  const [message, setMessage] = useState("");

  const handleClickStart = () => {
    SpeechRecognition.startListening();
  };

  const handleClickStop = () => {
    SpeechRecognition.stopListening;
    setMessage(transcript);
    resetTranscript();
  };

  if (!browserSupportsSpeechRecognition) {
    return <span>Browser doesn't support speech recognition.</span>;
  }

  return (
    <>
      <p>{message}</p>
      <button onClick={handleClickStart}>Start</button>
      <button onClick={handleClickStop}>Stop</button>
    </>
  );
}

export default App;

image.png

ここまで実装するとstartを押して、こんにちはと言い、stopを押すとこんにちはと表示されます

ここですこし音声入力の説明をしていきます。

  const {
    listening,
    transcript,
    resetTranscript,
    browserSupportsSpeechRecognition,
  } = useSpeechRecognition();

ここでhookを使っています

  • litening : 音声入力中かを表すフラグ (trueなら入力中)
  • transcript : 音声入力が終わったら、入力した文章が入る
  • resetTranscript : transcriptを初期化する
  • browserSupportsSpeechRecognition : ブラウザで音声入力が使えるかを表すフラグ(falseなら利用できない)
  const handleClickStart = () => {
    SpeechRecognition.startListening();
  };

  const handleClickStop = () => {
    SpeechRecognition.stopListening;
    setMessage(transcript);
    resetTranscript();
  };

  if (!browserSupportsSpeechRecognition) {
    return <span>Browser doesn't support speech recognition.</span>;
  }

Startを押したら、SpeechRecognitionのスタートを実行して音声を入力するようにして、stopを押したら入力を停止してから入力した文章をMessageにセットして初期化を行います

3. キャラクターが話せるようにする

今回は「ずんだもん」を利用して入力した音声を読んでもらうように実装していきます。

ずんだもんはAPIを利用することで簡単に使うことができます

まずはAPIキーを生成します

image.png

やり方に沿ってキーを取得してください

.envを作成してキーの情報を環境変数として読み込めるようにします

$ touch .env
.env
VITE_VOICEVOX_API_KEY=あなたのAPIキー

GithPushしないように.gitignoreに.envを追加しておきます

.gitignore
.env //最後の行に追加

VOICEVOXを使うにはGETリクエストをして、音声ファイルを再生することで簡単に利用が可能です。

src/App.tsx
  const handleClickStop = () => {
    SpeechRecognition.stopListening;
    setMessage(transcript);
    resetTranscript();
    playVoice(transcript);
  };

  const playVoice = async (message: string) => {
    const response = await fetch(
      `https://deprecatedapis.tts.quest/v2/voicevox/audio/?key=${
        import.meta.env.VITE_VOICEVOX_API_KEY
      }&speaker=0&pitch=0&intonationScale=1&speed=1&text=${message}`
    );

    const blob = await response.blob();
    const audio = new Audio(URL.createObjectURL(blob));
    audio.play();
  };

stopを押したら音声入力の内容をplayVoiceに渡しています。
palyVoiceではAPIを叩いて、音声ファイルを再生します

ここまで実装した上でアプリに音声入力をすると入力した内容をずんだもんが復唱してくれるアプリが完成します

4. ChatGPTの実装

ここからはChatGPTを活用して会話を行えるようにし、返答をすべてずんだもんが読み上げるように実装をします。

まずは、ChatGPTのAPIキーを取得します

左メニューのAPI Keysをクリック

image.png

create new Secret Keyを押してキーを取得します

image.png

.envに環境変数として設定します

.env
VITE_VOICEVOX_API_KEY=あなたのキー
VITE_OPENAI_API_KEY=あなたのキー

ChatGPTの実装をしていきます

src/App.tsx
import "regenerator-runtime";
import SpeechRecognition, {
  useSpeechRecognition,
} from "react-speech-recognition";
import { useState } from "react";

type Message = {
  role: "user" | "assistant";
  content: string;
};

function App() {
  const {
    listening,
    transcript,
    resetTranscript,
    browserSupportsSpeechRecognition,
  } = useSpeechRecognition();
  const [message, setMessage] = useState("");
  const [history, setHistory] = useState<Message[]>([]);

  const handleClickStart = () => {
    SpeechRecognition.startListening();
  };

  const handleClickStop = () => {
    SpeechRecognition.stopListening;
    setMessage(transcript);
    resetTranscript();
    sendGPT(transcript); // ChatGPTに送る
  };

  const sendGPT = async (message: string) => {
    const body = JSON.stringify({
      messages: [...history, { role: "user", content: message }],
      model: "gpt-4o", // GPT-4oを使用
    });
    const response = await fetch(`https://api.openai.com/v1/chat/completions`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${import.meta.env.VITE_OPENAI_API_KEY}`,
      },
      body,
    });

    const data = await response.json();
    const choice = data.choices[0].message.content;

    setHistory([...history, { role: "assistant", content: choice }]);
    playVoice(choice);
  };

  const playVoice = async (message: string) => {
    const response = await fetch(
      `https://deprecatedapis.tts.quest/v2/voicevox/audio/?key=${
        import.meta.env.VITE_VOICEVOX_API_KEY
      }&speaker=0&pitch=0&intonationScale=1&speed=1&text=${message}`
    );

    const blob = await response.blob();
    const audio = new Audio(URL.createObjectURL(blob));
    audio.play();
  };

  if (!browserSupportsSpeechRecognition) {
    return <span>Browser doesn't support speech recognition.</span>;
  }

  return (
    <>
      <p>{message}</p>
      <button onClick={handleClickStart}>Start</button>
      <button onClick={handleClickStop}>Stop</button>
    </>
  );
}

export default App;

ポイントを解説していきます

  const sendGPT = async (message: string) => {
    const body = JSON.stringify({
      messages: [...history, { role: "user", content: message }],
      model: "gpt-4o", // GPT-4oを使用
    });
    const response = await fetch(`https://api.openai.com/v1/chat/completions`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${import.meta.env.VITE_OPENAI_API_KEY}`,
      },
      body,
    });

    const data = await response.json();
    const choice = data.choices[0].message.content;

    setHistory([...history, { role: "assistant", content: choice }]);
    playVoice(choice);
  };

ここではChatGPTに入力した内容を投げています。
ポイントはbodymessagesです。

messagesは過去のメッセージと今回送りたいメッセージを配列にして送ることで、過去の知識をもったまま今回の質問に答えてくれるようになります。

1つの質問はMessage型で送る必要があります

type Message = {
  role: "user" | "assistant";
  content: string;
};

ここでroleは誰がその発言をしたのかを表します。userは私たちで、assistantはChatGPTの発言ということを表します。

過去の質問は保存しておく必要があるので、historyを利用しています

setHistory([...history, { role: "assistant", content: choice }]);

ここまで行うと、質問に対して返答を音声で返してくれるようになりました。

5. スタイルを整える

最後にスマートフォンで利用することを前提にスタイルを整えていきます。
ここは本質ではないので以下のスタイルをコピーして利用してみてください。

まずはアイコンを利用するためにreact-iconsをインストールします

$ npm i react-icons

App.tsxを以下にします

App.tsx
import "regenerator-runtime";
import SpeechRecognition, {
  useSpeechRecognition,
} from "react-speech-recognition";
import { useState } from "react";
import { FaSquare } from "react-icons/fa";
import { AiOutlineAudio } from "react-icons/ai";

type Message = {
  role: "user" | "assistant";
  content: string;
};

function App() {
  const {
    listening,
    transcript,
    resetTranscript,
    browserSupportsSpeechRecognition,
  } = useSpeechRecognition();
  const [history, setHistory] = useState<Message[]>([]);

  const handleClickStart = () => {
    SpeechRecognition.startListening();
  };

  const handleClickStop = () => {
    SpeechRecognition.stopListening;
    resetTranscript();
    sendGPT(transcript); // ChatGPTに送る
  };

  const sendGPT = async (message: string) => {
    const body = JSON.stringify({
      messages: [...history, { role: "user", content: message }],
      model: "gpt-4o", // GPT-4oを使用
    });
    const response = await fetch(`https://api.openai.com/v1/chat/completions`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${import.meta.env.VITE_OPENAI_API_KEY}`,
      },
      body,
    });

    const playVoice = async (message: string) => {
      const response = await fetch(
        `https://deprecatedapis.tts.quest/v2/voicevox/audio/?key=${
          import.meta.env.VITE_VOICEVOX_API_KEY
        }&speaker=0&pitch=0&intonationScale=1&speed=1&text=${message}`
      );

      const blob = await response.blob();
      const audio = new Audio(URL.createObjectURL(blob));
      audio.play();
    };

    if (!browserSupportsSpeechRecognition) {
      return <span>Browser doesn't support speech recognition.</span>;
    }

    const data = await response.json();
    const choice = data.choices[0].message.content;

    setHistory([...history, { role: "assistant", content: choice }]);
    playVoice(choice);
  };

  const buttonClasses = `mt-4 w-[60px] h-[60px] flex items-center justify-center text-2xl ${
    listening ? "bg-red-500" : "bg-blue-500"
  } rounded-full text-white`;
  const iconClass = listening ? <FaSquare /> : <AiOutlineAudio />;

  return (
    <>
      <div className="flex flex-col items-center w-full min-h-screen">
        <img
          src="https://ucarecdn.com/b6b3f827-c521-4835-a7d4-5db0c87c9818/-/format/auto/"
          alt="キャラクターの画像"
          className="w-full h-auto sm:w-full object-cover"
        />
        <div className="w-full max-w-[400px] sm:max-w-full sm:px-4 mt-4">
          <FramedImage
            characterName="ずんだもん"
            dialogueText={
              history[history.length - 1]
                ? history[history.length - 1].content
                : ""
            }
          />
        </div>
        {listening ? (
          <button onClick={handleClickStop} className={buttonClasses}>
            <i>{iconClass}</i>
          </button>
        ) : (
          <button onClick={handleClickStart} className={buttonClasses}>
            <i>{iconClass}</i>
          </button>
        )}
      </div>
    </>
  );
}

export default App;

返答の文章を表示するためのフレームをコンポーネントで用意します

$ touch src/FrameImage.tsx
src/FrameImage.tsx
import React from "react";

interface FramedImageProps {
  characterName: string;
  dialogueText: string;
}

const FramedImage: React.FC<FramedImageProps> = ({
  characterName,
  dialogueText,
}) => {
  return (
    <div className="relative w-full max-w-[360px] h-[190px] border-4 border-pink-500 mx-2 sm:px-2">
      <div className="absolute top-[-12px] left-[-4px] py-1 px-2 bg-pink-700 text-white text-sm font-bold font-roboto">
        {characterName}
      </div>
      <div className="absolute top-6 left-2 right-2 bottom-2 bg-white bg-opacity-80 text-black text-lg px-2 font-roboto overflow-y-auto">
        {dialogueText}
      </div>
    </div>
  );
};

export default FramedImage;

コンポーネントを作成したらApp.tsxでインポートしておきましょう

App.tsx
import { AiOutlineAudio } from "react-icons/ai";

ここまでできたらデベロッパーツールをひらいてiphone SEのレイアウトにします

Peek 2024-05-18 21-32.gif

音声入力するとずんだもんが答えてくれます!
ChatGPTが素早いので会話も比較的スムーズです!

おわりに

今回はChatGPTを利用してお手軽な音声チャットを実装しました
キャラクター変更などの機能を加えたり、ChatGPTに性格情報などを与えることでよりリアルな会話を楽しむことも可能です

ここまで読んでいただけた方はいいねとストックよろしくお願いします。
@Sicut_study をフォローいただけるととてもうれしく思います。

また明日の記事でお会いしましょう!

JISOUのメンバー募集中!

プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
実践的なカリキュラムで、あなたのエンジニアとしてのキャリアを最短で飛躍させましょう!
実践重視:即戦力を育てるアウトプット中心のプログラム。
モダンなスキル : Reactを中心としたモダンな技術を学べる。
キャリアアップ:スキルアップだけでなく、キャリアパスのサポートも充実。
興味のある方は、ぜひホームページからお気軽にカウンセリングをお申し込みください!
▼▼▼

90
104
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
90
104