実用レシピ集 ─ すぐ使える5つの実践チュートリアル
Step 1〜4 で学んだ知識を組み合わせ、実際のプロダクトや制作ワークフローにそのまま組み込める5つの実践レシピを用意しました。目的に近いレシピから試してみてください。
自分の声をクローンして、ブログや note 記事を自動朗読させる
「記事を書くだけで、自分の声のオーディオ版も同時に公開したい」というクリエイターやメディア運営者に最も人気の高いユースケースです。コスパと速度に優れた gemini-3.8-flash-lite-tts を使えば、5分(300秒)の記事でも約 $0.045(約7円)で自分の声のナレーションに変換できます。
Voice Replication Step 2:クローンした自分の声でブログ記事を読み上げる
作成された voice_...(または stateless な voicekey_...)を指定し、任意のテキストを自分の声質・話し方で合成した出力例
皆さんこんにちは!この記事の音声は、30秒のサンプル音声から複製した私自身のボイスクローンで自動生成しています。 <short pause> 収録マイクの前に座らなくても、テキストを書くだけでいつもの自分の声で配信できる時代になりました。
実装のポイント:長文記事を分割合成して「ブチッ」というノイズなく結合する
1回のリクエスト上限は出力 16,384 トークン(約10.9分)ですが、3,000〜5,000字以上の長文記事を安定して読み上げさせるには、見出し(H2)ごとに分割してリクエストし、1本の音声ファイルに結合するのがベストプラクティスです。
このとき、Gemini 3.8 TTS の一括出力(audio/wav)には先頭 44 バイトの RIFF WAV ヘッダが含まれるため、単純にバイト列を連結するとつなぎ目で「ブチッ」とクリックノイズが鳴ります。生 PCM(audio/l16)で受け取って最後に1回だけ WAV ヘッダを付けると、つなぎ目が完全にシームレスになります。
import base64
import wave
from google import genai
client = genai.Client()
def generate_article_audiobook(
paragraphs: list[str],
cloned_voice_id: str,
output_wav_path: str = "article_narration.wav"
) -> None:
"""クローンした自分の声で長文記事を段落ごとに合成し、1本のWAVにシームレス結合する"""
pcm_chunks: list[bytes] = []
for idx, text in enumerate(paragraphs, 1):
print(f"段落 {idx}/{len(paragraphs)} を合成中...")
interaction = client.interactions.create(
model="gemini-3.8-flash-lite-tts", # 大量生成・記事朗読は Flash-Lite が最適!
input=[{
"role": "user",
"content": [{
"type": "text",
"text": f"<breath> {text} <short pause>",
"speech_metadata": {
"style": "calm, articulate, and engaging blog narrator, steady pace"
}
}]
}],
generation_config={
"response_modalities": ["AUDIO"],
"speech_config": {
"voice": cloned_voice_id,
# ヘッダなしの 24kHz 16-bit PCM で受け取ることで結合時のクリックノイズをゼロにする
"response_format": {
"mime_type": "audio/l16",
"sample_rate": 24000
}
}
}
)
for out in interaction.outputs:
if out.type == "audio":
pcm_chunks.append(base64.b64decode(out.data))
# 全チャンクの PCM を結合し、先頭に1回だけ WAV ヘッダを書いて保存
with wave.open(output_wav_path, "wb") as wf:
wf.setnchannels(1) # モノラル
wf.setsampwidth(2) # 16-bit (2 bytes)
wf.setframerate(24000) # 24kHz
wf.writeframes(b"".join(pcm_chunks))
print(f"完了! {output_wav_path} を出力しました。")2人のAIホストが相づちを打ちながら解説するポッドキャスト番組を作る
NotebookLM の Audio Overview のような「2人のパーソナリティが自然に相づち(|うんうん|、|なるほど|)を重ねながら楽しく解説するラジオ番組」を、自社のニュース記事や社内報から自動生成するレシピです。相づちの重なりや笑いのニュアンスを最大限に引き出すため、ここはフラッグシップの gemini-3.8-flash-tts を使います。
2話者対話+パイプ相づち(|ええっ| |なるほど|)
セリフの中に |うんうん| や |うそだろ| を挟むだけで、ターンを分割せずに相手話者が背景で自然に相づちを重ねます
ミサキ: ねえケンタ、昨日の深夜に美術館の警備員が見回りをしたときは |うんうん| あの名画はまだ壁に掛かっていたそうなの。 ケンタ: えっ、じゃあ赤外線アラームは一度も鳴らなかったってこと? |一度もよ| いったいどうやって外に持ち出したんだ? ミサキ: <chuckle> 夜中に忍び込んだんじゃなかったのよ。 |えっ、まさか| 朝の9時に清掃員の作業着を着て |うそだろ| 正面玄関から堂々と運び出したんだって!
import base64
from pathlib import Path
from google import genai
client = genai.Client()
# 2人のホスト(Misaki: Aoede / Kenta: Puck)による掛け合い台本
dialogue_turns = [
{
"type": "text",
"text": "こんにちは!今週のテック深掘りラジオへようこそ。今回は音声生成AIの進化について語ります。",
"speech_metadata": {"speaker": "Misaki", "style": "bright, welcoming podcast host"}
},
{
"type": "text",
"text": "よろしくお願いします!いやあ、最近の音声モデルは本当にすごいですよね。",
"speech_metadata": {"speaker": "Kenta", "style": "friendly, curious co-host"}
},
{
"type": "text",
# パイプ |...| で囲んだ言葉は、Misaki が話している最中に Kenta の声で自然に重なる!
"text": "そうなんです。特に相づちが自然に重なるようになって |うんうん| まるで同じスタジオで向かい合って話しているみたいな |なるほど| 臨場感が出せるようになりました。<chuckle>",
"speech_metadata": {"speaker": "Misaki", "style": "enthusiastic and expressive"}
},
{
"type": "text",
"text": "確かに!一人が話し終わるのをただ待つだけじゃなくて、生きている会話って感じがしますね。",
"speech_metadata": {"speaker": "Kenta", "style": "impressed and upbeat"}
}
]
interaction = client.interactions.create(
model="gemini-3.8-flash-tts",
input=[{"role": "user", "content": dialogue_turns}],
generation_config={
"response_modalities": ["AUDIO"],
"speech_config": {
"speakers": [
{"speaker": "Misaki", "voice": "Aoede"},
{"speaker": "Kenta", "voice": "Puck"}
]
}
}
)
for out in interaction.outputs:
if out.type == "audio":
Path("podcast_episode.wav").write_bytes(base64.b64decode(out.data))ゲーム・アニメ・VTuber向けのドラマチックな感情セリフ演出
RPG のバトルボイスやノベルゲームのイベントシーンでは、「息を切らしながら叫び、直後に静かに息を吐いて決意を固める」といった激しい緩急が求められます。<pant>、<exhales>、<short pause> の3つのタグと style を組み合わせた実音声を聴いてみましょう。
レシピ3実演:ファンタジーRPGの気高い悪役令嬢ボイス
ゲーム・アニメ制作レシピ:冷ややかな貴族風のキャラクター演技に <sigh> と <chuckle> を織り交ぜたセリフ生成
<sigh> ずいぶんと可愛らしい反乱を企ててくれたものね。 <short pause> <chuckle> まあいいわ、私の寄木細工の床を汚す前に、まずはその泥だらけのブーツを拭いてちょうだい。
ゲーム制作での活用テクニック:バリエーションガチャを回す
同じプロンプトでも3〜4回生成すると、息継ぎの深さや声の裏返り方に微妙に異なるテイク(バリエーション)が生まれます。声優収録で「今のセリフ、もう1テイクお願いします!」とディレクションするのと同じ感覚で、ベストなテイクを採用しましょう。
低遅延ストリーミングと Gemini 3.5 Transcribe で作るリアルタイム音声対話エージェント
スマートスピーカーや接客・窓口 AI のように「ユーザーが話し終えたら即座に返答を話し始める」システムでは、全文の音声合成が終わるのを待っていては遅すぎます。
stream=True を有効にすると、生成された端から ヘッダなしの 24kHz 16-bit モノラル PCM チャンク(audio/l16) が次々に届きます。これを Step 1 で紹介した Gemini 3.5 Transcribe(SMART モード) と組み合わせることで、人間と遜色ないテンポの音声対話が完成します。
レシピ4・5実演:電話自動応答・リアルタイム音声エージェント(Flash-Lite)
Flash-Lite TTS による低遅延・高明瞭な電話応答サンプル(ストリーミング audio/l16 や電話回線用 audio/mulaw 8kHz 出力に対応)
お電話ありがとうございます、クラウドクリニック予約窓口でございます。 <short pause> 明日の午前10時ですね、空きがございますのでこのままご予約をお取りいたします。
import base64
from google import genai
client = genai.Client()
# ストリーミング音声合成(stream=True)
# ※ チャンクはヘッダなしの生 PCM(audio/l16, 24kHz, 16-bit mono)として順次届く
stream = client.interactions.create(
model="gemini-3.8-flash-lite-tts",
stream=True,
input=[{
"role": "user",
"content": [{
"type": "text",
"text": "お電話ありがとうございます、さくらクリニック予約窓口でございます。<breath> 本日は診察のご予約でしょうか、それとも予約日時のご変更でしょうか?",
"speech_metadata": {
"style": "polite, warm, and clear Japanese receptionist, natural conversational pace"
}
}]
}],
generation_config={
"response_modalities": ["AUDIO"],
"speech_config": {
"voice": "Kore"
}
}
)
for chunk in stream:
if getattr(chunk, "delta", None) and chunk.delta.type == "audio":
raw_pcm_bytes = base64.b64decode(chunk.delta.data)
# 届いた 24kHz 16-bit PCM チャンクをそのまま WebSocket やスピーカー出力へ流す!
print(f"受信チャンク: {len(raw_pcm_bytes)} bytes")Cloudflare Workers + R2 で作る「ゼロ遅延・超低コスト」音声キャッシュ API & 電話 IVR 連携
Web サイトの記事読み上げボタンやゲームの定型ボイス、電話自動応答(IVR)では、同じテキストの音声を毎回 API で再生成するのはコストも時間ももったいないですよね。
Cloudflare Workers($5 Paid プラン)と Cloudflare R2(エグレス通信料無料)を前段に置けば、以下の2つの強力なメリットが得られます。
- 1度生成した音声はエッジ(R2)から 15ミリ秒以下で即座に配信され、2回目以降の Gemini TTS API 課金が $0(完全無料)になる。
- Twilio 等の電話回線(IVR)に流す場合も、Gemini 3.8 TTS の
response_formatで"mime_type": "audio/mulaw", "sample_rate": 8000(またはaudio/alaw) を指定すれば、Workers 側で重い音声トランスコード処理を一切行わずに電話回線へ直結できる。
// src/pages/api/tts-cache.ts (Astro 7 + Cloudflare Workers / R2 の実装例)
import type { APIRoute } from 'astro';
// Astro 7 / @astrojs/cloudflare v14 では cloudflare:workers から直接 env を import する
import { env } from 'cloudflare:workers';
import { z } from 'astro/zod';
const RequestSchema = z.object({
text: z.string().min(1).max(2000),
voice: z.string().default('Kore'),
style: z.string().default('natural, clear Japanese narration'),
model: z.enum(['gemini-3.8-flash-tts', 'gemini-3.8-flash-lite-tts']).default('gemini-3.8-flash-lite-tts'),
// Web再生なら audio/wav (24kHz)、電話回線(Twilio等)なら audio/mulaw (8kHz)
format: z.enum(['wav24k', 'mulaw8k']).default('wav24k'),
});
export const POST: APIRoute = async ({ request }) => {
const parsed = RequestSchema.safeParse(await request.json());
if (!parsed.success) {
return new Response(JSON.stringify({ error: parsed.error.flatten() }), { status: 400 });
}
const { text, voice, style, model, format } = parsed.data;
// 1. パラメータの SHA-256 ハッシュを計算して R2 のキャッシュキーにする
const rawKey = JSON.stringify({ model, voice, style, text, format });
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(rawKey));
const hashHex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
const ext = format === 'mulaw8k' ? 'ulaw' : 'wav';
const r2Key = `tts-cache/${hashHex}.${ext}`;
// 2. R2 にキャッシュがあれば API を叩かずに即座に返す(課金ゼロ&超高速)
const cached = await env.TTS_AUDIO_BUCKET.get(r2Key);
if (cached) {
return new Response(cached.body, {
headers: {
'Content-Type': format === 'mulaw8k' ? 'audio/basic' : 'audio/wav',
'X-TTS-Cache': 'HIT',
},
});
}
// 3. キャッシュミス時は Gemini 3.8 TTS (Interactions API) で生成
const responseFormat =
format === 'mulaw8k'
? { mime_type: 'audio/mulaw', sample_rate: 8000 }
: { mime_type: 'audio/wav', sample_rate: 24000 };
const apiRes = await fetch('https://generativelanguage.googleapis.com/v1beta/interactions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': env.GEMINI_API_KEY,
},
body: JSON.stringify({
model,
input: [{
role: 'user',
content: [{ type: 'text', text, speech_metadata: { style } }],
}],
generation_config: {
response_modalities: ['AUDIO'],
speech_config: { voice, response_format: responseFormat },
},
}),
});
if (!apiRes.ok) {
return new Response(await apiRes.text(), { status: apiRes.status });
}
const data = (await apiRes.json()) as { outputs: Array<{ type: string; data?: string }> };
const audioOut = data.outputs.find((o) => o.type === 'audio');
if (!audioOut?.data) {
return new Response('Audio output missing', { status: 502 });
}
const audioBytes = Uint8Array.from(atob(audioOut.data), (c) => c.charCodeAt(0));
// 4. R2 バケットに保存して次回からエッジ配信
await env.TTS_AUDIO_BUCKET.put(r2Key, audioBytes, {
httpMetadata: { contentType: format === 'mulaw8k' ? 'audio/basic' : 'audio/wav' },
});
return new Response(audioBytes, {
headers: {
'Content-Type': format === 'mulaw8k' ? 'audio/basic' : 'audio/wav',
'X-TTS-Cache': 'MISS',
},
});
};