
子ども向けサイトの読み上げに Gemini TTS・Google Cloud TTS・Azure TTS・edge-tts を試した話
子ども向けの教育Webサイトを2つ運営しています。ひとつはどうぶつ・たべもの・のりものなどを学べるインタラクティブ図鑑、もうひとつはピクサー映画カーズのキャラクター図鑑です。
どちらもカードをタッチすると名前を読み上げる機能があるのですが、その音声品質を改善するために Web Speech API → Google Cloud TTS → Gemini TTS → Azure TTS → edge-tts と5つの手段を試した記録です。
なぜ音声読み上げが必要なのか
対象ユーザーはまだ文字が読めない幼児〜小学校低学年。画像と音声がセットになることで、ひらがなの読みや語彙の獲得を自然に促せます。
- 画像をタッチ → 名前が読み上げられる → 「これは"きりん"って言うんだ!」
- カーズのキャラクターをタッチ → 名前と説明文が読まれる → 映画をもっと楽しめる
音声の安定した品質と自然さは、子どもの集中力や学習意欲に直結します。
手段 1: Web Speech API(ブラウザ標準)
概要
APIを使わない場合、OSやブラウザに内蔵された音声合成エンジンを使うことになります。JavaScriptの SpeechSynthesisUtterance を使えば、サーバー不要・コスト0円で読み上げが実現できます。
const utterance = new SpeechSynthesisUtterance("きりん");
utterance.lang = "ja-JP";
utterance.rate = 0.9; // ゆっくりめ
utterance.pitch = 1.1; // 少し高めの声
// 高品質なネットワーク音声があれば優先
const voices = window.speechSynthesis.getVoices();
const networkVoice = voices.find(
(v) => v.lang.startsWith("ja") && !v.localService
);
if (networkVoice) utterance.voice = networkVoice;
window.speechSynthesis.speak(utterance);メリットとデメリット
| 項目 | 内容 |
|---|---|
| コスト | 無料 |
| 実装コスト | 低(クライアントのみで完結) |
| 音質 | OS/ブラウザ依存でばらつく |
| 安定性 | デバイスごとに挙動が異なる |
実際に使ってみた感想
macOS + Chrome では「Google 日本語(オンライン)」という比較的高品質な音声が利用できましたが、問題は環境依存です。
- macOS Safari: Appleの機械的な音声のみ。品質にかなり差がある
- iOS Safari: ユーザー操作なしに発話できない制限あり
- Android: デバイスメーカーによって音声が全く異なる
つまり、「自分の環境ではOK」でもユーザーの環境では全然違う音質になる。子ども向けサイトで「端末によって声が変わる」のは体験として良くないため、API移行を決めました。
手段 2: Google Cloud Text-to-Speech(Chirp 3: HD)
概要
Googleが提供するクラウドTTSサービスです。REST APIにテキストを投げると、高品質な音声データが返ってきます。
POST https://texttospeech.googleapis.com/v1/text:synthesize?key={API_KEY}
APIの呼び出しコード
const VOICE = "ja-JP-Wavenet-B";
async function generateMp3(text: string, apiKey: string): Promise<Buffer> {
const res = await fetch(
`https://texttospeech.googleapis.com/v1/text:synthesize?key=${apiKey}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
input: { text },
voice: {
languageCode: "ja-JP",
name: VOICE,
},
audioConfig: {
audioEncoding: "MP3",
speakingRate: 0.9,
pitch: 1.0,
},
}),
}
);
if (!res.ok) {
throw new Error(`API エラー ${res.status}: ${await res.text()}`);
}
const json = await res.json();
return Buffer.from(json.audioContent, "base64");
}レスポンスはBase64エンコードされたMP3データ。デコードすればそのまま再生可能です。
料金体系
| 音声タイプ | 無料枠 | 超過後 |
|---|---|---|
| Standard | 月400万文字 | $4/100万文字 |
| WaveNet | 月100万文字 | $16/100万文字 |
| Neural2 | 月100万文字 | $16/100万文字 |
| Chirp 3: HD | 月100万文字 | $30/100万文字 |
子ども向けサイトの読み上げ程度なら、無料枠内で十分収まります。
バッチ生成スクリプト
毎回APIを叩くのは非効率なので、全アイテムの音声を事前にMP3ファイルとして生成しておくスクリプトを作りました。
const items = getAllItems(); // 全211アイテム
const total = items.length;
for (let i = 0; i < items.length; i++) {
const { text, filePath } = items[i];
// 既存ファイルはスキップ
if (existsSync(filePath)) {
console.log(`[${i+1}/${total}] スキップ`);
continue;
}
const mp3 = await generateMp3(text, apiKey);
await writeFile(filePath, mp3);
// レート制限対策
await new Promise(r => setTimeout(r, 200));
}一度生成すれば静的ファイルとして配信できるため、ランタイムでのAPI呼び出しは不要。コストも初回生成の1回分だけです。
実際の感想
WaveNet・Chirp 3: HD どちらも品質は十分。Web Speech APIとは雲泥の差で、環境に依存しない安定した品質を提供できます。ただし、「子どもに読んであげている」ような温かみのある声かというと、まだ少し機械的に感じる場面もありました。
手段 3: Gemini TTS(本命)
概要
Googleの生成AIモデル Gemini にTTS機能が搭載されました。従来のTTSとは異なり、LLMが音声を生成するため非常にナチュラルな発話が特徴です。
POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent
APIの呼び出しコード
async function generateWithGemini(
text: string,
apiKey: string,
voice: string = "Kore"
): Promise<Buffer> {
const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-goog-api-key": apiKey,
},
body: JSON.stringify({
contents: [{
parts: [{
text: `次のテキストをそのまま読み上げてください:${text}`
}]
}],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: { voiceName: voice },
},
},
},
}),
}
);
if (!res.ok) throw new Error(`Gemini API エラー: ${await res.text()}`);
const json = await res.json();
const b64 = json.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
if (!b64) throw new Error("音声データが取得できませんでした");
// Gemini TTSはPCM(24kHz, mono, 16bit)を返すのでWAVヘッダーを付与
const pcm = Buffer.from(b64, "base64");
return Buffer.concat([buildWavHeader(pcm.byteLength), pcm]);
}Cloud TTSとの違い: WAVヘッダーの手動構築
Gemini TTSはMP3ではなく生のPCMデータをBase64で返します。ブラウザで再生するには自前でWAVヘッダーを付ける必要があります。
function buildWavHeader(pcmByteLength: number): Buffer {
const sampleRate = 24000;
const channels = 1;
const bitsPerSample = 16;
const byteRate = (sampleRate * channels * bitsPerSample) / 8;
const blockAlign = (channels * bitsPerSample) / 8;
const header = Buffer.alloc(44);
header.write("RIFF", 0);
header.writeUInt32LE(36 + pcmByteLength, 4);
header.write("WAVE", 8);
header.write("fmt ", 12);
header.writeUInt32LE(16, 16); // fmt チャンクサイズ
header.writeUInt16LE(1, 20); // PCM
header.writeUInt16LE(channels, 22);
header.writeUInt32LE(sampleRate, 24);
header.writeUInt32LE(byteRate, 28);
header.writeUInt16LE(blockAlign, 32);
header.writeUInt16LE(bitsPerSample, 34);
header.write("data", 36);
header.writeUInt32LE(pcmByteLength, 40);
return header;
}この44バイトのヘッダー構築は外部ライブラリ不要で実装できます。
プリセット音声(8種類)
Gemini TTSには8つのプリセット音声が用意されています。
| 音声名 | 性別 | 日本語での印象 |
|---|---|---|
| Aoede | 女性 | 明るく聞き取りやすい |
| Kore | 女性 | 落ち着いた標準的なトーン |
| Leda | 女性 | 柔らかく優しい |
| Zephyr | 女性 | さわやかで軽快 |
| Charon | 男性 | 低めで落ち着き |
| Fenrir | 男性 | 力強い |
| Orus | 男性 | 深みのある声 |
| Puck | 男性 | 軽快で親しみやすい |
子ども向けコンテンツには Kore(落ち着いた女性の声)や Leda(柔らかく優しい声)が適していました。
スタイル制御
Gemini TTSには speakingRate や pitch のような数値パラメータがありません。代わりに、プロンプトの指示文で読み方を制御できます。
「ゆっくりと優しく読んでください:きりん」
「元気よく明るく:ライトニング・マックィーン」
LLMならではのアプローチです。数値で微調整するというよりも、自然言語で「こう読んでほしい」と伝える形です。
Geminiの読み上げはナチュラル!
実際に使い比べて一番驚いたのが、Gemini TTSの自然さです。Cloud TTSのWaveNetやChirp 3: HDと比べても、明らかに「人が読んでいる」ように聞こえます。
- 抑揚が自然で、機械的な棒読み感がない
- 日本語の固有名詞の読み方が正確(「マックィーン」を正しく発音する)
- 文の区切りやイントネーションが自然
子どもに聞かせたとき、Cloud TTSでは「機械が喋ってる」と言われることがありましたが、Gemini TTSではそのような反応がなくなりました。
ただし、課題もある
Gemini TTSは現時点でプレビュー段階のため、いくつかネックがあります。
1. レートリミット
HTTP 429: Resource has been exhausted
短時間に連続してリクエストすると、すぐにレートリミットに引っかかります。バッチ生成スクリプトでは 2秒のウェイト を入れて対処しましたが、それでもクォータ超過で中断されることがありました。
const DELAY_MS = 2000; // リクエスト間の待機時間(レートリミット対策)
// クォータ超過時は翌日に再実行
if (res.status === 429) {
console.log("クォータ超過。明日以降に再実行してください");
process.exit(0);
}2. 従量課金
| 項目 | 料金 |
|---|---|
| テキスト入力 | $0.30/100万トークン |
| 音声出力 | $2.50/100万トークン |
Cloud TTSと比べると「文字数」ではなく「トークン数」での課金になるため、コスト比較が難しい面があります。音声出力のトークン料金は決して安くないため、キャッシュの活用が必須です。
実装アーキテクチャ: サーバーサイドキャッシュ
Gemini TTSのレートリミットとコスト問題に対処するため、Next.js API Route + ファイルキャッシュのアーキテクチャを採用しました。
ブラウザ(カードをクリック)
↓
POST /api/tts { text, provider: "gemini", voice: "Kore" }
↓
Next.js API Route
├── キャッシュあり → .cache/tts/{hash}.wav を即座に返却
│
└── キャッシュなし → Gemini API を呼び出し
↓
PCMデータ(Base64)
↓
WAVヘッダーを付与
↓
.cache/tts/{hash}.wav に保存 → 返却
キャッシュキー
テキスト・プロバイダー・音声名の組み合わせをSHA-256でハッシュ化しています。
function cacheFilePath(provider: string, voice: string, text: string): string {
const hash = createHash("sha256")
.update(`${provider}:${voice}:${text}`)
.digest("hex");
const ext = provider === "gemini" ? "wav" : "mp3";
return path.join(CACHE_DIR, `${hash}.${ext}`);
}同じテキスト×声の組み合わせは常に同じ音声になるため、キャッシュに有効期限は不要です。一度生成すれば二度とAPIを叩く必要がありません。
クライアント側の再生
const res = await fetch("/api/tts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, provider: "gemini", voice: "Kore" }),
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
audio.onended = () => URL.revokeObjectURL(url);
audio.play();APIキーはサーバーサイドのみで保持し、クライアントには一切公開しません。
手段 4: Azure Speech Service(Microsoft のクラウド TTS)
概要
Microsoftが提供するクラウドTTSサービスです。Azure AI Services の一部として提供されており、REST APIまたはSDK経由で利用できます。日本語のニューラル音声が複数用意されており、SSMLによる細やかな読み方制御が可能です。
POST https://{region}.tts.speech.microsoft.com/cognitiveservices/v1
APIの呼び出しコード
Azure TTSはSSML(Speech Synthesis Markup Language)形式でリクエストを送ります。
const REGION = "japaneast";
const VOICE = "ja-JP-NanamiNeural";
async function generateWithAzure(
text: string,
apiKey: string
): Promise<Buffer> {
const ssml = `
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="ja-JP">
<voice name="${VOICE}">
<prosody rate="-10%" pitch="+5%">
${text}
</prosody>
</voice>
</speak>`;
const res = await fetch(
`https://${REGION}.tts.speech.microsoft.com/cognitiveservices/v1`,
{
method: "POST",
headers: {
"Ocp-Apim-Subscription-Key": apiKey,
"Content-Type": "application/ssml+xml",
"X-Microsoft-OutputFormat": "audio-16khz-128kbitrate-mono-mp3",
},
body: ssml,
}
);
if (!res.ok) {
throw new Error(`Azure TTS エラー ${res.status}: ${await res.text()}`);
}
return Buffer.from(await res.arrayBuffer());
}Google Cloud TTSとの大きな違いは、レスポンスがそのままバイナリ音声データで返ってくる点です。Base64デコードは不要で、受け取ったデータをそのままMP3ファイルとして保存・再生できます。
日本語対応の音声
| 音声名 | ShortName | 性別 | 特徴 |
|---|---|---|---|
| Nanami | ja-JP-NanamiNeural | 女性 | 標準的で聞き取りやすい |
| Keita | ja-JP-KeitaNeural | 男性 | 落ち着いた男性の声 |
| Aoi | ja-JP-AoiNeural | 女性 | 明るく親しみやすい |
| Daichi | ja-JP-DaichiNeural | 男性 | 低めで安定感がある |
| Mayu | ja-JP-MayuNeural | 女性 | 柔らかく優しい |
| Naoki | ja-JP-NaokiNeural | 男性 | さわやか |
| Shiori | ja-JP-ShioriNeural | 女性 | 穏やか |
| Masaru (HD) | ja-JP-MasaruMultilingualNeural | 男性 | 多言語対応・HD音声 |
2025年に追加された Masaru Dragon HD は、感情検出や多言語対応に優れた最新のHD音声モデルです。
料金体系
| カテゴリ | 無料枠 | 従量課金 |
|---|---|---|
| Neural | 月50万文字 | $15/100万文字 |
| Custom Neural | - | $24/100万文字 |
Google Cloud TTSと比較すると、無料枠はやや少ない(50万文字 vs 100万文字)ですが、従量課金は同程度の水準です。
SSMLによる細かな制御
Azure TTSの強みはSSMLの充実度です。Google Cloud TTSもSSMLに対応していますが、Azureは音声スタイルの切り替えが特に優れています。
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="http://www.w3.org/2001/mstts" xml:lang="ja-JP">
<voice name="ja-JP-NanamiNeural">
<prosody rate="-15%" pitch="+10%">
きりん
</prosody>
</voice>
</speak>実際の感想
日本語の音質は Google Cloud TTS の WaveNet と同等かそれ以上。特に Nanami は安定して聞き取りやすく、子ども向けコンテンツにも適しています。ただし、Gemini TTSのような「人が語りかけている」自然さには及ばない印象です。
Azureを選ぶ最大の理由は、既にMicrosoft / Azure のエコシステムを使っている場合の統合のしやすさです。Azure Functions や App Service と組み合わせた構成が容易で、エンタープライズ環境に向いています。
手段 5: edge-tts(無料で使える Microsoft Edge の音声)
概要
edge-tts は、Microsoft Edgeブラウザが内部的に使用しているオンラインTTSサービスを、APIキーなし・完全無料で利用できるPython製のオープンソースツールです。GitHubで10,000以上のスターを獲得しています。
実体としては Azure Speech Service と同じニューラル音声エンジンを使用しているため、Azure TTSとほぼ同等の音質が無料で手に入るという点が最大の魅力です。
インストールと基本的な使い方
pip install edge-ttsCLIで手軽に音声を生成できます。
# 日本語の音声ファイルを生成
edge-tts --voice ja-JP-NanamiNeural --text "きりん" --write-media kirin.mp3
# 字幕ファイル(SRT)も同時に生成
edge-tts --voice ja-JP-NanamiNeural --text "こんにちは、世界!" \
--write-media hello.mp3 --write-subtitles hello.srt
# 利用可能な音声一覧を表示
edge-tts --list-voicesPythonコードからの利用
import asyncio
import edge_tts
VOICE = "ja-JP-NanamiNeural"
async def generate_audio(text: str, output_file: str) -> None:
communicate = edge_tts.Communicate(text, VOICE)
await communicate.save(output_file)
asyncio.run(generate_audio("きりん", "kirin.mp3"))速度・ピッチの調整
# ゆっくり読み上げ
edge-tts --voice ja-JP-NanamiNeural --rate=-30% --text "きりん" --write-media slow.mp3
# 高い声で
edge-tts --voice ja-JP-NanamiNeural --pitch=+20Hz --text "きりん" --write-media high.mp3バッチ生成スクリプト
Pythonのバッチスクリプトで全アイテムの音声を一括生成する例です。
import asyncio
import os
import edge_tts
VOICE = "ja-JP-NanamiNeural"
OUTPUT_DIR = "audio"
items = [
{"text": "きりん", "file": "kirin.mp3"},
{"text": "ぞう", "file": "zou.mp3"},
{"text": "らいおん", "file": "lion.mp3"},
]
async def generate_all():
os.makedirs(OUTPUT_DIR, exist_ok=True)
for i, item in enumerate(items):
path = os.path.join(OUTPUT_DIR, item["file"])
if os.path.exists(path):
print(f"[{i+1}/{len(items)}] スキップ: {item['text']}")
continue
communicate = edge_tts.Communicate(item["text"], VOICE)
await communicate.save(path)
print(f"[{i+1}/{len(items)}] 生成完了: {item['text']}")
asyncio.run(generate_all())メリットとデメリット
| 項目 | 内容 |
|---|---|
| コスト | 完全無料(APIキー不要) |
| 音質 | Azure Neural と同等(高品質) |
| 出力形式 | MP3(直接保存可能) |
| 言語 | Python(CLIまたはライブラリ) |
| 安定性 | Microsoft のサービス仕様変更に依存 |
| SRT字幕 | 同時生成可能 |
注意点
edge-tts はMicrosoft Edge ブラウザが内部的に利用しているエンドポイントを叩いているため、公式にサポートされたAPIではありません。
- Microsoftがエンドポイントの仕様を変更すると動かなくなる可能性がある
- 商用利用のライセンスは明確ではない(ライブラリ自体はGPL-3.0)
- レートリミットは明確に公開されていないが、大量リクエストはブロックされる可能性がある
とはいえ、個人プロジェクトや開発・検証用途では非常に有用です。Azure TTSの音声を試して品質を確認し、本番では正規のAzure APIに切り替えるという使い方が現実的です。
5つの手段の比較まとめ
| Web Speech API | Cloud TTS (Chirp 3: HD) | Gemini TTS | Azure TTS | edge-tts | |
|---|---|---|---|---|---|
| 音質 | 環境依存 | 高品質・安定 | 非常に自然 | 高品質・安定 | Azure と同等 |
| コスト | 無料 | 月100万文字まで無料 | トークン従量課金 | 月50万文字まで無料 | 完全無料 |
| 安定性 | デバイスごとに異なる | 一定 | 一定 | 一定 | サービス変更リスクあり |
| レートリミット | なし | 緩め | 厳しい(プレビュー) | 緩め | 非公開 |
| 認証 | 不要 | APIキー | APIキー | APIキー | 不要 |
| 出力形式 | ブラウザ直接再生 | MP3/WAV | PCM(WAV変換必要) | MP3/WAV | MP3 |
| SSML対応 | 限定的 | あり | なし(プロンプト制御) | 充実 | rate/pitch のみ |
| 言語 | JavaScript | 任意(REST) | 任意(REST) | 任意(REST/SDK) | Python |
| おすすめ用途 | プロトタイプ | 安定運用 | 音質最優先 | エンタープライズ | 開発・検証・個人利用 |
使い分けの指針
Web Speech API を選ぶべき場面
- プロトタイプや個人利用
- コストをかけたくない場合
- 音質にこだわらない場合
Google Cloud TTS を選ぶべき場面
- 安定した品質で多くのユーザーに提供したい場合
- SSML(読み方のマークアップ)を細かく制御したい場合
- 無料枠の範囲で運用したい場合
Gemini TTS を選ぶべき場面
- ナチュラルな音声が最優先の場合
- 読み上げるテキストの種類が限定的で、キャッシュ戦略が有効な場合
- 今後のGA(一般提供)を見据えた先行投資として
Azure TTS を選ぶべき場面
- Microsoft / Azure エコシステムを既に使っている場合
- SSMLで細かく読み方を制御したい場合
- エンタープライズ向けのSLA・サポートが必要な場合
edge-tts を選ぶべき場面
- コスト0で高品質な音声を得たい場合
- 開発中の検証や音声の試聴として
- Pythonのバッチ処理で大量に音声ファイルを生成したい場合
- 本番環境では正規APIに切り替える前提のプロトタイピング
今後の展望
Gemini TTSは現在プレビュー段階ですが、GAになればレートリミットの緩和と料金の最適化が期待できます。現時点のベストプラクティスは以下の通りです。
- Gemini TTSでキャッシュを事前生成(最高品質の音声を確保)
- キャッシュミス時はGoogle Cloud TTSにフォールバック(安定性を担保)
- 開発中・ローカル環境では edge-tts で手軽にAzure品質の音声を確認
- エンタープライズ環境ではAzure TTSも有力な選択肢
「APIを使うことで環境依存をなくし、安定した品質を提供できる」という基本と、「Gemini TTSの自然さは従来のTTSとは別次元」という発見が今回の大きな収穫でした。そして edge-tts の存在は、コストを気にせず音声品質を試せるという点で、開発の初期段階で特に価値があります。