【Google I/O 2025】 Gemini API ＋ Node.js で音声合成（TTS）を試す

## はじめに

Google I/O 2025 で発表されていたものの 1つ、Gemini API を使った音声合成（TTS）を試してみます。

●音声生成（テキスト読み上げ）  |  Gemini API  |  Google AI for Developers
　https://ai.google.dev/gemini-api/docs/speech-generation?hl=ja#javascript

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/0401da04-7251-440a-a497-43a320033d50.png)


### 直近で試したこと： Google I/O 2025関連

Google I/O 2025 で発表されていたものは、直近で他にも試しています。具体的には以下になりますが、今回もそれらと同様 Node.js を使って試します。

●Gemini API ＆ SDK が MCP 対応したので試す（Node.js を利用）【Google I/O 2025】 - Qiita
　https://qiita.com/youtoy/items/fd1b123c3f7fc3516264

●【Google I/O 2025】 API の無料枠で Gemini・Gemma の新モデルを試す（Node.js を利用） - Qiita
　https://qiita.com/youtoy/items/714a1bd58a80f856663c

あと、まだ記事にはできてないですが、以下の音楽生成も Node.js で試しました。

https://x.com/youtoy/status/1925523938089869697


## サクッと試す

それでは実際に試していきます。

### 下準備

下準備として、パッケージのインストールと APIキーの設定を行います。

#### パッケージのインストール

公式ドキュメントの最初のサンプルを見てみたところ、以下 2つのパッケージを使うようです。

●wav - npm
　https://www.npmjs.com/package/wav

●@google/genai - npm
　https://www.npmjs.com/package/@google/genai

これらを以下のコマンドでインストールします。


```zsh
npm i @google/genai wav
```

#### APIキーの設定

次に APIキーの設定です。環境変数 GEMINI_API_KEY に Gemini の APIキーをセットしておきます。


### コードと処理の実行

公式ドキュメントに掲載されている、以下のコードを試してみます。

```javascript
import { GoogleGenAI } from "@google/genai";
import wav from "wav";

async function saveWaveFile(
  filename,
  pcmData,
  channels = 1,
  rate = 24000,
  sampleWidth = 2
) {
  return new Promise((resolve, reject) => {
    const writer = new wav.FileWriter(filename, {
      channels,
      sampleRate: rate,
      bitDepth: sampleWidth * 8,
    });

    writer.on("finish", resolve);
    writer.on("error", reject);

    writer.write(pcmData);
    writer.end();
  });
}

async function main() {
  const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

  const response = await ai.models.generateContent({
    model: "gemini-2.5-flash-preview-tts",
    contents: [{ parts: [{ text: "Say cheerfully: Have a wonderful day!" }] }],
    config: {
      responseModalities: ["AUDIO"],
      speechConfig: {
        voiceConfig: {
          prebuiltVoiceConfig: { voiceName: "Kore" },
        },
      },
    },
  });

  const data = response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
  const audioBuffer = Buffer.from(data, "base64");

  const fileName = "out.wav";
  await saveWaveFile(fileName, audioBuffer);
}
await main();
```

#### 音声合成用のモデル

モデルは「[gemini-2.5-flash-preview-tts](https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview-tts)」が使われています。ちなみに、もう 1つ「[gemini-2.5-pro-preview-tts](https://ai.google.dev/gemini-api/docs/models#gemini-2.5-pro-preview-tts)」というモデルも使えるようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/9b5290a8-9ee1-4771-b7c4-2af30405b68d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/613fef07-2df6-444e-ae5a-a7545a4880b9.png)


#### 処理結果

上記を実行してみたところ、「out.wav」というファイルが生成されました。それを再生すると「Have a wonderful day!」という明るい声を聞くことができました。



### 2つ目のお試し

[別のサンプル（複数のスピーカーによるテキスト読み上げ）](https://ai.google.dev/gemini-api/docs/speech-generation?hl=ja#multi-speaker)を少し書きかえて試してみます。

下準備（パッケージのインストールと APIキーの設定）は、先ほどと同じです。


#### 声の種類

この後試す音声合成の声の種類を、公式サンプルで使われているもの（voiceName：「Kore」「Puck」）とは別のものに変えてみることにします。

その部分を変える際の候補の参照先として、公式ドキュメント上に書かれている声の種類を見てみます。また、それと重複しますが、Google AI Studio上で選べる声の種類の一部を、以下に合わせて掲載してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/43a8ba59-1b37-4bd6-932e-e14ff369238a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/c17db9a1-e5d8-46bc-b846-cc28dfa21918.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/ce58a482-3ad4-4612-9528-9c78899a7575.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/a7f0412c-013e-4021-9810-b01624151572.png)

この中から 2種類の声を選ぶことにします。


#### プロンプトでの音声スタイルを制御

プロンプトでの音声スタイルを制御についても、情報を見てみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/4eb004bd-447f-44bf-ba31-39d6469f9a84.png)

これもこの後に使ってみます。


#### コード

試すコードは以下のとおりです。

声の種類、しゃべる内容を変えていて、さらに音声スタイルを制御するプロンプトを加えています。

```javascript
import { GoogleGenAI } from "@google/genai";
import wav from "wav";

async function saveWaveFile(
  filename,
  pcmData,
  channels = 1,
  rate = 24000,
  sampleWidth = 2
) {
  return new Promise((resolve, reject) => {
    const writer = new wav.FileWriter(filename, {
      channels,
      sampleRate: rate,
      bitDepth: sampleWidth * 8,
    });

    writer.on("finish", resolve);
    writer.on("error", reject);

    writer.write(pcmData);
    writer.end();
  });
}

async function main() {
  const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

  const prompt = `話し声がすごくゆっくりなスピーカー1と、退屈そうで声が小さいスピーカー2の、2人の会話の音声合成を行ってください:
         スピーカー1: 今日は良い天気ですね。元気にしてますか？
         スピーカー2: 悪くはないかな。あなたこそどうですか？`;

  const response = await ai.models.generateContent({
    model: "gemini-2.5-flash-preview-tts",
    contents: [{ parts: [{ text: prompt }] }],
    config: {
      responseModalities: ["AUDIO"],
      speechConfig: {
        multiSpeakerVoiceConfig: {
          speakerVoiceConfigs: [
            {
              speaker: "スピーカー1",
              voiceConfig: {
                prebuiltVoiceConfig: { voiceName: "Autonoe" },
              },
            },
            {
              speaker: "スピーカー2",
              voiceConfig: {
                prebuiltVoiceConfig: { voiceName: "Umbriel" },
              },
            },
          ],
        },
      },
    },
  });

  const data = response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
  const audioBuffer = Buffer.from(data, "base64");

  const fileName = "out02.wav";
  await saveWaveFile(fileName, audioBuffer);
}

await main();
```


#### wavファイルを生成して再生

上記を実行して wavファイルを生成し、それを再生します。wavファイルを再生している時の音を、キャプチャして動画にしてみました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/jFq8ywsMWfA?si=OKzofKmoFHqQ_Bqd" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

こんな感じの合成された音声を得ることができました。



## 【追記】 別サンプルでのお試し

記事を公開した後、Google AI Studio で別のサンプルが出てきたので、それも試してみました。具体的には、以下の部分をクリックすると表示されるサンプルです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/d3feb9e1-d72d-41f9-bce7-063ab558dce9.png)

選べる言語などは、以下となっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/ec5fc57f-541b-4894-9322-2f4ba65eddc8.png)


### 下準備

この後のコードを実行する前の下準備についてふれておきます。

以下はパッケージのインストールです。

```zsh
npm i @google/genai mime
```

あとは APIキーの設定です。環境変数 GEMINI_API_KEY に Gemini の APIキーをセットしておきます。


### 実装したコード： JavaScript

実装したコードは以下で、TypeScript のサンプルコードを JavaScript に変更しています。

```javascript
import { GoogleGenAI } from "@google/genai";
import mime from "mime";
import { writeFile } from "fs";

function saveBinaryFile(fileName, content) {
  writeFile(fileName, content, "utf8", (err) => {
    if (err) {
      console.error(`Error writing file ${fileName}:`, err);
      return;
    }
    console.log(`File ${fileName} saved to file system.`);
  });
}

async function main() {
  const ai = new GoogleGenAI({
    apiKey: process.env.GEMINI_API_KEY,
  });

  const config = {
    temperature: 1,
    responseModalities: ["audio"],
    multiSpeakerVoiceConfig: {
      speakerVoiceConfigs: [
        {
          speaker: "Speaker 1",
          voiceConfig: { prebuiltVoiceConfig: { voiceName: "Zephyr" } },
        },
        {
          speaker: "Speaker 2",
          voiceConfig: { prebuiltVoiceConfig: { voiceName: "Puck" } },
        },
      ],
    },
  };

  const model = "gemini-2.5-flash-preview-tts";
  const contents = [
    {
      role: "user",
      parts: [
        {
          text: `Read aloud in a warm, welcoming tone
Speaker 1: Hello! We're excited to show you our native speech capabilities
Speaker 2: Where you can direct a voice, create realistic dialog, and so much more. Edit these placeholders to get started.`,
        },
      ],
    },
  ];

  const response = await ai.models.generateContentStream({
    model,
    config,
    contents,
  });

  for await (const chunk of response) {
    const cand = chunk.candidates?.[0]?.content;
    if (!cand?.parts) continue;
    const part = cand.parts[0];

    if (part.inlineData) {
      const fileName = "test";
      const { mimeType, data } = part.inlineData;
      let fileExtension = mime.getExtension(mimeType || "");
      let buffer = Buffer.from(data || "", "base64");

      if (!fileExtension) {
        fileExtension = "wav";
        buffer = convertToWav(data || "", mimeType || "");
      }

      saveBinaryFile(`${fileName}.${fileExtension}`, buffer);
    } else if (chunk.text) {
      console.log(chunk.text);
    }
  }
}

main();

function convertToWav(rawData, mimeType) {
  const options = parseMimeType(mimeType);
  const wavHeader = createWavHeader(rawData.length, options);
  const audioBuffer = Buffer.from(rawData, "base64");
  return Buffer.concat([wavHeader, audioBuffer]);
}

function parseMimeType(mimeType) {
  const [fileType, ...params] = mimeType.split(";").map((s) => s.trim());
  const [, format] = fileType.split("/");
  const options = { numChannels: 1 };

  if (format && format.startsWith("L")) {
    const bits = parseInt(format.slice(1), 10);
    if (!isNaN(bits)) options.bitsPerSample = bits;
  }

  for (const param of params) {
    const [key, value] = param.split("=").map((s) => s.trim());
    if (key === "rate") options.sampleRate = parseInt(value, 10);
  }

  return options;
}

function createWavHeader(dataLength, options) {
  const { numChannels, sampleRate, bitsPerSample } = options;
  const byteRate = (sampleRate * numChannels * bitsPerSample) / 8;
  const blockAlign = (numChannels * bitsPerSample) / 8;
  const buffer = Buffer.alloc(44);

  buffer.write("RIFF", 0);
  buffer.writeUInt32LE(36 + dataLength, 4);
  buffer.write("WAVE", 8);
  buffer.write("fmt ", 12);
  buffer.writeUInt32LE(16, 16);
  buffer.writeUInt16LE(1, 20);
  buffer.writeUInt16LE(numChannels, 22);
  buffer.writeUInt32LE(sampleRate, 24);
  buffer.writeUInt32LE(byteRate, 28);
  buffer.writeUInt16LE(blockAlign, 32);
  buffer.writeUInt16LE(bitsPerSample, 34);
  buffer.write("data", 36);
  buffer.writeUInt32LE(dataLength, 40);

  return buffer;
}
```

これを実行して、合成された音声の wavファイルが生成されたのを確認できました。


### curl を使ったサンプル

ちなみに、先ほどの選択肢で REST を選んだ場合、以下の内容が提示されました。


```zsh
#!/bin/bash
set -e -E

GEMINI_API_KEY="$GEMINI_API_KEY"
MODEL_ID="gemini-2.5-flash-preview-tts"
GENERATE_CONTENT_API="streamGenerateContent"

cat << EOF > request.json
{
    "contents": [
      {
        "role": "user",
        "parts": [
          {
            "text": "Read aloud in a warm, welcoming tone\nSpeaker 1: Hello! We're excited to show you our native speech capabilities\nSpeaker 2: Where you can direct a voice, create realistic dialog, and so much more. Edit these placeholders to get started."
          },
        ]
      },
    ],
    "generationConfig": {
      "responseModalities": ["audio", ],
      "temperature": 1,
      "speech_config": {
        "multi_speaker_voice_config": {
          "speaker_voice_configs": [
            {
              "speaker": "Speaker 1",
              "voice_config": {
                "prebuilt_voice_config": {
                  "voice_name": "Zephyr"
                }
              }
            },
            {
              "speaker": "Speaker 2",
              "voice_config": {
                "prebuilt_voice_config": {
                  "voice_name": "Puck"
                }
              }
            },
          ]
        },
      },
    },
}
EOF

curl \
-X POST \
-H "Content-Type: application/json" \
"https://generativelanguage.googleapis.com/v1beta/models/${MODEL_ID}:${GENERATE_CONTENT_API}?key=${GEMINI_API_KEY}" -d '@request.json'
```