0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Google I/O 2025】 Live API の「Gemini 2.5 Flash Native Audio(モデル: gemini-2.5-flash-preview-native-audio-dialog)」を試す

Last updated at Posted at 2025-05-25

はじめに

Google I/O 2025 で色々新しい発表があったので、それを試して記事を書いているシリーズがあり、この記事はそのうちの 1つです。

あと記事は書けてないですが、「Node.js で API を使った音楽生成(Lyria RealTime による)」も試してたりします。

今回の内容

今回はタイトルに書いてある、Live API の「Gemini 2.5 Flash Native Audio」を使った、「テキストに対する音声での応答」を試します。モデルは「gemini-2.5-flash-preview-native-audio-dialog」を使います。

公式情報

今回試す「Gemini 2.5 Flash Native Audio」の公式情報として、以下があります。

https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-native-audio
image.png

指定できるモデルは、以下 2つがあるようです。

  • gemini-2.5-flash-preview-native-audio-dialog
  • gemini-2.5-flash-exp-native-audio-thinking-dialog

また入出力は、以下に対応しているようです。

  • 入力: Audio, video, text
  • 出力: Audio and text

また、Gemini 2.5 Flash Native Audio の情報を含む Live API の情報がのっている公式ドキュメントのページは、以下があります。

●Live API  |  Gemini API  |  Google AI for Developers
 https://ai.google.dev/gemini-api/docs/live
image.png

現状、プレビュー版の API です。

モデルの違い

Live API用のモデルについてツールも利用可能です。ただし、モデルによって使えるツールに違いがあるようで、それについては以下に掲載されています。

https://ai.google.dev/gemini-api/docs/live#tools-overview
image.png

サンプルコード

今回のお試しを進めるための、参照用の情報にするサンプルコードを探します。

今回は、モデルを使ったやりとりが試せる以下の Google AI Studio でコードを確認しました。

●Stream | Google AI Studio
 https://aistudio.google.com/live

具体的には、Live API のモデルを試せる画面に進み、その画面内で、以下で示したアイコンをクリックします。

image.png

そうすると、記事執筆時点では Python と TypeScript のサンプルコードを見ることができます。

image.png

自分は、以下の TypeScript のコードを参照用の情報として使いました。

// To run this code you need to install the following dependencies:
// npm install @google/genai mime
// npm install -D @types/node
import {
  GoogleGenAI,
  LiveServerMessage,
  MediaResolution,
  Modality,
  Session,
} from '@google/genai';
import mime from 'mime';
import { writeFile } from 'fs';
const responseQueue: LiveServerMessage[] = [];
let session: Session | undefined = undefined;

async function handleTurn(): Promise<LiveServerMessage[]> {
  const turn: LiveServerMessage[] = [];
  let done = false;
  while (!done) {
    const message = await waitMessage();
    turn.push(message);
    if (message.serverContent && message.serverContent.turnComplete) {
      done = true;
    }
  }
  return turn;
}

async function waitMessage(): Promise<LiveServerMessage> {
  let done = false;
  let message: LiveServerMessage | undefined = undefined;
  while (!done) {
    message = responseQueue.shift();
    if (message) {
      handleModelTurn(message);
      done = true;
    } else {
      await new Promise((resolve) => setTimeout(resolve, 100));
    }
  }
  return message!;
}

const audioParts: string[] = [];
function handleModelTurn(message: LiveServerMessage) {
  if(message.serverContent?.modelTurn?.parts) {
    const part = message.serverContent?.modelTurn?.parts?.[0];

    if(part?.fileData) {
      console.log(`File: ${part?.fileData.fileUri}`);
    }

    if (part.inlineData) {
      const fileName = 'ENTER_FILE_NAME';
      const inlineData = part.inlineData;

      audioParts.push(inlineData.data || '');

      const buffer = convertToWav(audioParts, inlineData.mimeType || '');
      saveBinaryFile(`${fileName}.wav`, buffer);
    }

    if(part?.text) {
      console.log(part?.text);
    }
  }
}

function saveBinaryFile(fileName: string, content: Buffer) {
  writeFile(fileName, content, 'utf8', (err) => {
    if (err) {
      console.error(`Error writing file ${fileName}:`, err);
      return;
    }
    console.log(`Appending stream content to file ${fileName} .`);
  });
}

interface WavConversionOptions {
  numChannels : number,
  sampleRate: number,
  bitsPerSample: number
}

function convertToWav(rawData: string[], mimeType: string) {
  const options = parseMimeType(mimeType);
  const dataLength = rawData.reduce((a, b) => a + b.length, 0);
  const wavHeader = createWavHeader(dataLength, options);
  const buffer = Buffer.concat(rawData.map(data => Buffer.from(data, 'base64')));

  return Buffer.concat([wavHeader, buffer]);
}

function parseMimeType(mimeType : string) {
  const [fileType, ...params] = mimeType.split(';').map(s => s.trim());
  const [_, format] = fileType.split('/');

  const options : Partial<WavConversionOptions> = {
    numChannels: 1,
    bitsPerSample: 16,
  };

  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 as WavConversionOptions;
}

function createWavHeader(dataLength: number, options: WavConversionOptions) {
  const {
    numChannels,
    sampleRate,
    bitsPerSample,
  } = options;

  // http://soundfile.sapp.org/doc/WaveFormat

  const byteRate = sampleRate * numChannels * bitsPerSample / 8;
  const blockAlign = numChannels * bitsPerSample / 8;
  const buffer = Buffer.alloc(44);

  buffer.write('RIFF', 0);                      // ChunkID
  buffer.writeUInt32LE(36 + dataLength, 4);     // ChunkSize
  buffer.write('WAVE', 8);                      // Format
  buffer.write('fmt ', 12);                     // Subchunk1ID
  buffer.writeUInt32LE(16, 16);                 // Subchunk1Size (PCM)
  buffer.writeUInt16LE(1, 20);                  // AudioFormat (1 = PCM)
  buffer.writeUInt16LE(numChannels, 22);        // NumChannels
  buffer.writeUInt32LE(sampleRate, 24);         // SampleRate
  buffer.writeUInt32LE(byteRate, 28);           // ByteRate
  buffer.writeUInt16LE(blockAlign, 32);         // BlockAlign
  buffer.writeUInt16LE(bitsPerSample, 34);      // BitsPerSample
  buffer.write('data', 36);                     // Subchunk2ID
  buffer.writeUInt32LE(dataLength, 40);         // Subchunk2Size

  return buffer;
}

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

  const model = 'models/gemini-2.5-flash-preview-native-audio-dialog'

  const config = {
    responseModalities: [
        Modality.AUDIO,
    ],
    mediaResolution: MediaResolution.MEDIA_RESOLUTION_MEDIUM,
    speechConfig: {
      voiceConfig: {
        prebuiltVoiceConfig: {
          voiceName: 'Zephyr',
        }
      }
    },
    contextWindowCompression: {
        triggerTokens: '25600',
        slidingWindow: { targetTokens: '12800' },
    },
  };

  session = await ai.live.connect({
    model,
    callbacks: {
      onopen: function () {
        console.debug('Opened');
      },
      onmessage: function (message: LiveServerMessage) {
        responseQueue.push(message);
      },
      onerror: function (e: ErrorEvent) {
        console.debug('Error:', e.message);
      },
      onclose: function (e: CloseEvent) {
        console.debug('Close:', e.reason);
      },
    },
    config
  });

  session.sendClientContent({
    turns: [
      `INSERT_INPUT_HERE`
    ]
  });

  await handleTurn();

  session.close();
}
main();

これを書きかえて使っていきます。

今回試した内容

今回試した内容について書いていきます。

コード

今回、元のサンプルコードを JavaScript のものに変えて使いました。また、プロンプトの部分は、日本語の内容に変えています(あと、出力する wavファイルのファイル名も test という内容で設定しました)。

具体的には以下の内容です。

import { GoogleGenAI, MediaResolution, Modality } from "@google/genai";
import mime from "mime";
import { writeFile } from "fs";

const responseQueue = [];
let session;

async function handleTurn() {
  const turn = [];
  let done = false;
  while (!done) {
    const message = await waitMessage();
    turn.push(message);
    if (message.serverContent && message.serverContent.turnComplete) {
      done = true;
    }
  }
  return turn;
}

async function waitMessage() {
  let message;
  while (true) {
    message = responseQueue.shift();
    if (message) {
      handleModelTurn(message);
      return message;
    }
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
}

const audioParts = [];
function handleModelTurn(message) {
  const parts = message.serverContent?.modelTurn?.parts;
  if (!parts) return;
  const part = parts[0];

  if (part.fileData) {
    console.log(`File: ${part.fileData.fileUri}`);
  }

  if (part.inlineData) {
    const fileName = "test";
    audioParts.push(part.inlineData.data || "");
    const buffer = convertToWav(audioParts, part.inlineData.mimeType || "");
    saveBinaryFile(`${fileName}.wav`, buffer);
  }

  if (part.text) {
    console.log(part.text);
  }
}

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

function convertToWav(rawData, mimeType) {
  const options = parseMimeType(mimeType);
  const dataLength = rawData.reduce((sum, chunk) => sum + chunk.length, 0);
  const header = createWavHeader(dataLength, options);
  const body = Buffer.concat(rawData.map((d) => Buffer.from(d, "base64")));
  return Buffer.concat([header, body]);
}

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

  if (format && format.startsWith("L")) {
    const bits = parseInt(format.slice(1), 10);
    if (!isNaN(bits)) opts.bitsPerSample = bits;
  }
  for (const param of params) {
    const [key, val] = param.split("=").map((s) => s.trim());
    if (key === "rate") {
      opts.sampleRate = parseInt(val, 10);
    }
  }
  return opts;
}

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

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

  return buf;
}

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

  const model = "models/gemini-2.5-flash-preview-native-audio-dialog";

  const config = {
    responseModalities: [Modality.AUDIO],
    mediaResolution: MediaResolution.MEDIA_RESOLUTION_MEDIUM,
    speechConfig: {
      voiceConfig: {
        prebuiltVoiceConfig: {
          voiceName: "Zephyr",
        },
      },
    },
    contextWindowCompression: {
      triggerTokens: "25600",
      slidingWindow: { targetTokens: "12800" },
    },
  };

  session = await ai.live.connect({
    model,
    callbacks: {
      onopen() {
        console.debug("Opened");
      },
      onmessage(msg) {
        responseQueue.push(msg);
      },
      onerror(e) {
        console.debug("Error:", e.message);
      },
      onclose(e) {
        console.debug("Close:", e.reason);
      },
    },
    config,
  });

  session.sendClientContent({
    turns: ["自己紹介をしてください。4つの文章で。"],
  });

  await handleTurn();
  session.close();
}

main().catch(console.error);

あとは、上記を試すための下準備をします。

下準備

上記のコードを実行するため、パッケージのインストールと APIキーの設定を行います。

以下でパッケージをインストールします。

npm i @google/genai mime

それと環境変数 GEMINI_API_KEY に、Gemini の APIキーをセットしておきます。

処理を実行してみる

今回用意したコードを実行して、こちらの日本語の文に対する応答を音声データ(wavファイル)として得ます。その音声データを再生した時の様子は以下の動画のとおりです。

プロンプトは「自己紹介をしてください。4つの文章で。」という内容にしていたのですが、それに対する音声での応答を確認できました。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?