🦜

Next.jsでPollyが生成した音声をブラウザ再生する

に公開

はじめに

Pollyとは、Amazonが提供する音声生成サービスです。テキストを入力として人間の音声を生成が可能で、日本語にも対応しています。

本記事では、次の構成にてPollyで生成した音声をブラウザで再生します。
AWS CLIとプロファイルの指定が完了している前提とします。

  • Next.js + TypeScript
  • API で Polly を実行し、フロントに音声データを返却
  • フロントで音声データを再生

なお、本記事は社内ハッカソン内で実施した技術検証となります。作成したプロダクトについては別記事にまとめております。
https://zenn.dev/ncdc/articles/21b5cb55fc498f

APIでPollyを実行する

インストール

Pollyのライブラリをインストールします。

npm install @aws-sdk/client-polly

環境変数の設定

AWS CLIとプロファイルの指定が完了している前提のため、必要な環境変数のみを指定します。

AWS_REGION=ap-northeast-1
AWS_PROFILE={プロファイル名}

REGIONには東京リージョンを指定しています。別のリージョンが使いたい場合は変更が必要です。

API作成

Pollyは複数の声の生成に対応しています。ここでは最も流暢だった Neural Kazuha を指定しています。

/api/polly.ts
const AWS_REGION = process.env.AWS_REGION;
const AWS_PROFILE = process.env.AWS_PROFILE;

const pollyClient = new PollyClient({
  region: AWS_REGION,
  credentials: fromIni({ profile: AWS_PROFILE }),
});

const params = {
  OutputFormat: "mp3" as const,
  VoiceId: "Kazuha" as const,
  Engine: "neural" as const,
};

export async function POST(request: NextRequest) {
  try {
    const formData = await request.formData();
    const inputText = formData.get("inputText") as string;

    const command = new SynthesizeSpeechCommand({
      ...params,
      Text: inputText,
    });
    const data = await pollyClient.send(command);

    if (!(data.AudioStream instanceof Readable)) {
      throw new Error("AudioStream 取得エラー");
    }

    const chunks: Buffer[] = [];
    for await (const chunk of data.AudioStream) {
      chunks.push(chunk);
    }
    const audioBuffer = Buffer.concat(chunks);
    return NextResponse.json({
      audioData: audioBuffer.toString("base64"),
    });
  } catch (error) {
    return NextResponse.json(
      { error: "サーバーエラー", details: String(error) },
      { status: 500 },
    );
  }
}

これにより、/api/pollyのPOSTメソッドが有効となります。

フロントで音声データを再生

作成したエンドポイントにリクエストを実行し、レスポンスに含まれる音声データを再生します。
ここでは、ボタンを押下すると固定文言の音声が再生される処理を記述します。

"use client";

export default function Home() {
  const onClick = async () => {
    const formData = new FormData();
    formData.append(
      "inputText",
      "こんにちは。Pollyを使った音声生成テストです。",
    );
    // リクエストを実行
    const response = await fetch("/api/polly", {
      method: "POST",
      body: formData,
    });

    if (response.ok) {
      const data = await response.json();
      if (data.audioData) {
        // Base64をBlobに変換
        const byteCharacters = atob(data.audioData);
        const byteNumbers = Array.from(byteCharacters, (char) =>
          char.charCodeAt(0),
        );
        const byteArray = new Uint8Array(byteNumbers);
        const audioBlob = new Blob([byteArray], { type: "audio/mpeg" });
        const audioObjectUrl = URL.createObjectURL(audioBlob);
        
        const audio = new Audio(audioObjectUrl);
        // 音声再生終了時、URLを無効化する
        audio.addEventListener('ended', () => {
          URL.revokeObjectURL(audioObjectUrl);
        });
        // 音声の再生を開始
        audio.play().catch(error => {
          // 再生失敗時にURLを無効化する
          URL.revokeObjectURL(audioObjectUrl);
        });
      }
    }
  };

  return (
    <main>
      <button onClick={onClick}>Polly再生</button>
    </main>
  );
}

音声データ形式変換の流れ

Pollyで生成された音声は、ブラウザで再生されるまでに複数回形式を変換されています。

API上の変換

APIからPollyを呼び出したとき、音声はMP3で生成されています。

const params = {
  OutputFormat: "mp3" as const,

Polly実行後、音声はReadableストリームによって操作可能となります。
forによるイテレータを利用して、データの分割単位であるチャンクを順に処理していきます。
これにより、Bufferとしてバイナリデータにまとめることができます。

    for await (const chunk of data.AudioStream) {
      chunks.push(chunk);
    }
    const audioBuffer = Buffer.concat(chunks);

最後に、音声のバイナリデータをbase64で文字列化して返却します。

    return NextResponse.json({
      audioData: audioBuffer.toString("base64"),
    });

ブラウザ上の変換

クライアントでは。base64で文字列化された音声データの受け取りから処理が始まります。
base64文字列からバイナリデータにデコード後、数値配列、バイト配列、Blobオブジェクトへと変換しています。
Blob変換時に指定しているMIMEタイプaudio/mpegは、MP3のデータ形式を指します。

        const byteCharacters = atob(data.audioData);
        const byteNumbers = Array.from(byteCharacters, (char) =>
          char.charCodeAt(0),
        );
        const byteArray = new Uint8Array(byteNumbers);
        const audioBlob = new Blob([byteArray], { type: "audio/mpeg" });

最後に、BlobへアクセスするURLを生成します。
こちらをAudioに指定することで、音声がブラウザで再生可能となります。

        const audioObjectUrl = URL.createObjectURL(audioBlob);
        const audio = new Audio(audioObjectUrl);

なお、URLは不要となったタイミングでメモリ解放を行うことが推奨されます。

        audio.addEventListener('ended', () => {
          URL.revokeObjectURL(audioObjectUrl);
        });
        audio.play().catch(error => {
          URL.revokeObjectURL(audioObjectUrl);
        });
NCDCエンジニアブログ

Discussion