🐕

OpenAI の Text to Speech API を使って音声を再生

2024/10/03に公開

はじめに

この記事では、OpenAI の Text to Speech API を使って、入力されたテキストを音声に変換して再生する方法を紹介します。

完成品

今回は、こちらを作成します。

https://www.youtube.com/watch?v=S2JaD7G-Bdw

リポジトリ

作業リポジトリはこちらです。

https://github.com/hayato94087/next-speech-synthesis-demo

プロジェクトの作成

まずは、Next.js プロジェクトを作成します。

npx create-next-app@latest next-speech-synthesis-demo \
                              --typescript \
                              --eslint \
                              --import-alias "@/*" \
                              --src-dir \
                              --use-pnpm \
                              --tailwind \
                              --app \
                              --use-pnpm
cd next-speech-synthesis-demo
code .

OpenAI API キーの取得

こちらの記事を参考に、OpenAI API キーを取得します。

https://zenn.dev/hayato94087/articles/85378e1f7bc0e5#openai-の-apiキーの取得

環境変数の設定

環境変数に OpenAI キーを追加します。<your-api-key> に自身の API キーを設定してください。

$ touch .env .env.example

.gitignore.env を追加します。

.gitignore
# local env files
.env*.local
+.env
.env
# OPENAI_API_KEY は OpenAI の API キーです。
OPENAI_API_KEY='<your-api-key>'

ついでに、.env.example を作成します。

touch .env.example
.env.example
# OPENAI_API_KEY は OpenAI の API キーです。
OPENAI_API_KEY=

コミットします。

git add . && git commit -m "環境変数を作成"

OpenAI SDK のインストール

OpenAI SDK をインストールします。

pnpm add openai

コミットします。

git add . && git commit -m "OpenAI SDK をインストール"

Server Action を作成

テキストを引数に取り、音声を合成して base64 エンコードした文字列を返す synthesizeSpeech 関数を作成します。

touch src/app/actions.ts
actions.ts
'use server'

import OpenAI from 'openai'

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

export async function synthesizeSpeech(text: string): Promise<string> {
  const mp3 = await openai.audio.speech.create({
    model: "tts-1",
    voice: "alloy",
    input: text,
  })

  const buffer = Buffer.from(await mp3.arrayBuffer())
  return buffer.toString('base64')
}

コミットします。

git add . && git commit -m "Server Action を作成"

コンポーネントを作成

2 つのコンポーネントを作成します。

mkdir -p src/components
touch src/components/demo-1.tsx
touch src/components/demo-2.tsx
demo-1.tsx
'use client'

import { useState } from 'react'
import { synthesizeSpeech } from '@/app/actions'

export const Demo1 = () => {
  const [audioSrc, setAudioSrc] = useState<string | null>(null)
  const [inputText, setInputText] = useState<string>('こんにちは!元気ですか!')
  const [isLoading, setIsLoading] = useState<boolean>(false)

  const handleSynthesize = async () => {
    if (!inputText.trim()) return
    setIsLoading(true)
    try {
      const base64Audio = await synthesizeSpeech(inputText)
      setAudioSrc(`data:audio/mp3;base64,${base64Audio}`)
    } catch (error) {
      console.error('Error synthesizing speech:', error)
      alert('Failed to synthesize speech. Please try again.')
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <div className="flex flex-col items-center justify-center bg-gray-100 px-8 py-8 rounded-lg">
      <h1 className="mb-6 self-start text-xl font-bold ">音声データを生成</h1>
      <p className="mb-4 self-start text-slate-800">音声を生成したいテキストを入力してください</p>
      <div className="w-full max-w-md space-y-4">
        <div className="flex flex-row space-x-3">
          <input
            type="text"
            value={inputText}
            onChange={(e) => setInputText(e.target.value)}
            placeholder="音声を生成したいテキストを入力してください"
            className="w-full px-3 py-2 rounded-md text-slate-800"
          />
          <button
            onClick={handleSynthesize} 
            className="bg-slate-800 py-2 px-2 rounded-lg text-white w-[200px]"
            disabled={isLoading || !inputText.trim()}
          >
            {isLoading ? '音声合成中...' : '音声を生成'}
          </button>
        </div>
        {audioSrc && (
          <audio controls src={audioSrc} className="w-full">
            Your browser does not support the audio element.
          </audio>
        )}
      </div>
    </div>
  )
}
demo-2.tsx
"use client";

import { useState, useCallback } from "react";
import { synthesizeSpeech } from "../app/actions";

export const Demo2 = () => {
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [inputText, setInputText] =
    useState<string>("こんにちは!元気ですか!");

  const playAudio = useCallback(async (base64Audio: string) => {
    const AudioContextClass: typeof AudioContext =
      window.AudioContext ||
      (window as unknown as { webkitAudioContext: typeof AudioContext })
        .webkitAudioContext;
    const audioContext = new AudioContextClass();
    const arrayBuffer = Uint8Array.from(atob(base64Audio), (c) =>
      c.charCodeAt(0)
    ).buffer;
    const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
    const source = audioContext.createBufferSource();
    source.buffer = audioBuffer;
    source.connect(audioContext.destination);
    source.start(0);
  }, []);

  const handleSynthesize = async () => {
    if (!inputText.trim()) return;
    setIsLoading(true);
    try {
      const base64Audio = await synthesizeSpeech(inputText);
      await playAudio(base64Audio);
    } catch (error) {
      console.error("Error synthesizing or playing speech:", error);
      alert("Failed to synthesize or play speech. Please try again.");
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="flex flex-col items-center justify-center bg-gray-100 px-8 py-8 rounded-lg">
      <h1 className="mb-6 self-start text-xl font-bold ">
        音声データを即時再生
      </h1>
      <p className="mb-4 self-start text-slate-800">
        音声を再生したいテキストを入力してください
      </p>
      <div className="w-full max-w-md space-y-4">
        <div className="flex flex-row space-x-3">
          <input
            type="text"
            value={inputText}
            onChange={(e) => setInputText(e.target.value)}
            placeholder="Enter text to synthesize"
            aria-label="Text to synthesize"
            className="w-full px-3 py-2 rounded-md text-slate-800"
          />
          <button
            onClick={handleSynthesize}
            disabled={isLoading || !inputText.trim()}
            className="bg-slate-800 py-2 px-2 rounded-lg text-white w-[200px]"
          >
            {isLoading ? '音声生成中...' : '音声再生'}
            </button>
        </div>
      </div>
    </div>
  );
};

コミットします。

git add . && git commit -m "コンポーネントを作成"

ページを作成

page.tsx を作成します。

page.tsx
import { Demo1 } from "@/components/demo-1";
import { Demo2 } from "@/components/demo-2";

export default function Home() {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen space-y-4">
      <Demo1 />
      <Demo2 />
    </div>
  );
}

コミットします。

git add . && git commit -m "ページを作成"

動作確認

動作確認をします。

pnpm run dev

http://localhost:3000/

https://www.youtube.com/watch?v=S2JaD7G-Bdw

まとめ

この記事では、OpenAI の Text to Speech API を使って、入力されたテキストを音声に変換して再生する方法を紹介しました。

Discussion