📝 この記事は forge.workstyle.tech に掲載した記事の転載です。
Gemini TTSで2人の会話を作ると、台詞ごとの話者と演技指示を渡す読み上げリクエストで400エラーになりました。声の作成には成功していましたが、読み上げに使った入力形式が合っていませんでした。原因はInteractions APIに渡す台詞とannotationsの形式でした。
先に結論
Gemini TTSで2人の会話を作るとき、声の作成後にgenerateContentのspeechConfigへ声IDを渡す方法はどちらも400(BadRequest)になり、通ったのはPOST /v1beta/interactionsでした。400の原因は台詞ごとの話者やannotationsの形式で、speech_metadata注釈に話者と演技指示を入れ、speech_config.speakersで話者と声を対応させると生成できました。
声の基本設計はどこに書くか
声を作るリクエストはPOST /v1beta/voicesです。リクエストの骨格は次のとおりです。
{
"store": true,
"voice": {
"model": "...",
"type": "prompted",
"display_name": "...",
"gender": "male",
"language_code": "ja-JP",
"prompted": {
"input": "声の説明文"
}
}
}
prompted.inputには、声の高さや話し方、感情による変化を書きます。たとえば、次のような説明です。
70代前半の日本人男性。普段は穏やかで柔らかい、少し高めのよく通る声。作品の面白さの話になると声の温度が下がり、低く静かな凄みが出る。ゆっくり、はっきり、間を大事にする話し方。標準語。
ここで決めるのは声の基本的な設計です。会話中の台詞ごとの演技は、読み上げ時に別途指定します。
返り値にはid、model、type、expire_time、display_name、prompted、language_code、gender、usage、sample_audioが含まれます。sample_audioは試聴用の音声です。IDはvoice_で始まります。実際の値は公開せず、読み上げのリクエストには作成したIDを使います。
2人の声はInteractions APIでどう指定するか
Interactions APIは、台詞などの入力を渡して音声を生成するAPIです。作成した声のIDをgenerateContentのspeechConfigに渡す方法は2通り試しましたが、どちらも400(BadRequest)になりました。通ったのはPOST /v1beta/interactionsです。
1人の台詞なら、リクエストは次の形です。
{
"model": "...",
"input": "台詞",
"generation_config": {
"speech_config": [
{
"voice": "voice_..."
}
]
}
}
2人の会話では、generation_config.speech_configで話者と声を対応させます。
{
"model": "...",
"input": [
{
"type": "text",
"text": "面白い?",
"annotations": [
{
"type": "speech_metadata",
"speaker": "B",
"style": "褒め言葉ではない。面白いかも確かめずに帰ろうとする相手への皮肉と凄み。低く、ゆっくり、一拍おいて。目が笑っていない声。明るく言わない"
}
]
}
],
"generation_config": {
"speech_config": {
"speakers": [
{
"speaker": "A",
"voice": "voice_..."
},
{
"speaker": "B",
"voice": "voice_..."
}
]
}
}
}
inputは台詞ごとの配列にします。各台詞にはtype: textとtextを置き、annotations(台詞に付ける情報の配列)にはtype: speech_metadata、話者、演技指示を入れます。さらに、speech_config.speakersで話者AとBをそれぞれの声に結びつけます。
400エラーはどの順で解消したか
最初の400は、次のエラーでした。
Multi-speaker interactions must specify a speaker for each text turn.
複数話者を指定したのに、台詞ごとの話者を指定していませんでした。台詞の入力にも話者を結びつける必要がありました。
次は、input[7].annotationsにオブジェクトを渡したため、配列が必要だというエラーです。
The value is invalid for 'input[7].annotations'. Expected an array, got object.
そこでannotationsを配列にしました。ところが、話者の情報を台詞オブジェクトに直接置いたため、今度は次のエラーになりました。
Unknown parameter 'speaker' at 'input[7]'.
話者名は台詞オブジェクトに直接置かず、annotationsの要素として持たせる必要がありました。要素を追加したところ、型も必要だと分かりました。
The 'type' parameter is required at 'input[7].annotations[0]'.
そこでtypeを加えました。ただし、注釈の種類には正しい値を指定できていませんでした。
Unknown parameter 'speech_metadata' at 'input[7].annotations[0]'.
最終的に、注釈を{"type": "speech_metadata", "speaker": "B", "style": "演技指示"}の形にしました。5つのエラーから、台詞の話者、注釈の配列、注釈の型と値という入力形式の問題を順に確認できました。
演技指示は台詞ごとに変える
声の説明文と台詞の演技指示では、用途が違います。前者には声の基本的な性質を、後者にはその場面の感情や話し方を書きます。
今回の演技指示では、単に「皮肉っぽく」とはせず、相手が何をしたか、どんな温度で返すかを文章にしました。低く、ゆっくり、一拍おくことや、明るく言わないことも指定しています。会話全体の声は文章で設計し、各台詞の注釈に演技指示を置く構成です。
返り値の音声はsteps[].content[].dataにbase64(データを文字列で表す形式)で含まれます。形式はaudio/wavで、サンプルレートは24kHzでした。
生成音声を台詞単位で使うには
1回で生成した会話は12.84秒でした。ffmpeg(音声を処理するツール)のsilencedetectを使い、noise=-40dB、d=0.15で無音区間を検出して、8つの台詞に切り分けました。
口パクの工程では、台本の各台詞に対応する音声ファイルを持たせました。合成をとばし、その台詞用の音声ファイルを使う形にしました。
生成音声は文字起こしでも確認しました。話者名や演技指示が読み上げられていないことを確認しています。
既製の声で作った会話は約9秒でした。文章から設計した声の会話は12.8〜14.7秒で、最後の台詞の前に間が入りました。比較した範囲では、声の設計と台詞ごとの演技指示を使った会話のほうが長くなっていました。
使わなかった方法と未確認の点
30秒の音声から声を複製する機能は使いませんでした。実在の人物の声を真似ない決まりと両立させるため、文章から新しい声を作る方式にしました。
費用は、使った量から10セント前後と見積もりましたが、請求画面では確認していません。ボイスデザインそのものに料金がかかるかも確認していません。声の保存期限はドキュメントに「1年」とありますが、返り値のexpire_timeが実際にどの値だったかは確かめていません。これらは確認済みの結果として扱わないようにします。
まとめ
- 声の作成には
POST /v1beta/voicesを使い、返り値のvoice_で始まるIDを控える - 読み上げでは
generateContentのspeechConfigではなく、通ったPOST /v1beta/interactionsの形を確認する - 複数話者では台詞ごとに
annotationsのtypeとspeakerを指定する -
generation_config.speech_config.speakersで話者AとBを声に結びつける - 生成後は文字起こしで話者名や演技指示が読み上げられていないか確認する