[Live API] Function declaration size degrades speech output (name pronunciation): is there a documented budget or design pattern?
メンテナーはふだん 1 日以内に返信
@Venkaiahbabuneelam がすでに取り組んでいます。
2026年10月14日 から。
評価
調査の方向性
Start with case2_name_rendering_repro.py: filler_tools builds the padding declarations and system_instruction builds the padded prompt. Run the control command (--prompt-chars 1756, no --tool-bytes) and the --tool-bytes 1700 command with --grade, then listen to the WAVs in the out-dir. Done would mean a reproducible, graded difference that points to a documented size or design limit, or a confirmed model-side behavior with a clear workaround.
索引モデルが issue の本文から書いたものです。
説明
Environment
- Model:
gemini-3.8-live - Endpoints: Gemini Developer API (API key) and Vertex AI (
vertexai=True, locationus, application default credentials). Project ID available privately if useful. - SDK:
google-genai2.24.0, Python - Compared against:
gemini-3.1-flash-live-preview, same script
Configuration
types.LiveConnectConfig(
response_modalities=["AUDIO"],
system_instruction=SYSTEM, # ~1,750 chars: one line naming the agent, then neutral padding
tools=[types.Tool(function_declarations=FILLER)], # 11 unused declarations, ~1,729 bytes of JSON
temperature=0.7,
speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Leda"))),
input_audio_transcription=types.AudioTranscriptionConfig(),
output_audio_transcription=types.AudioTranscriptionConfig(),
)
The greeting is triggered with one send_client_content message (role="user", turn_complete=True) asking the model to say a fixed greeting word for word. The greeting says the name three times: "Hi, this is Hannah, Juan's assistant. Juan can't come to the phone right now, but I can take a message. Would you like Juan to call you back?"
Problem
With the declarations present, gemini-3.8-live often pronounces "Juan" with an English onset: "John", "Jwahn", "Jahn", or "Yahn" instead of the Spanish "Hwahn". The declarations are never called and have nothing to do with the name. The same system instruction without them pronounces the name correctly every time. output_transcription shows the correct spelling throughout, so only the audio is affected. gemini-3.1-flash-live-preview pronounces it correctly at this size, but it shows the same behavior at larger declaration sizes (below).
We build a phone assistant that greets callers on behalf of a named person, so this mispronounces our customer's own name to their callers. Keeping the declarations small enough to avoid it limits what the assistant can do in a session.
Results (attached script, 2026-10-12, 20 sessions per arm unless noted)
Sessions in which the name was mispronounced:
| Condition | API 3.8 | Vertex 3.8 | API 3.1 (n=10) |
|---|---|---|---|
| ~1,750-char instruction, no declarations | 0 of 20 | 0 of 20 | 0 of 10 |
| same + 11 unused declarations (1,729 bytes) | 10 of 20 (p=0.0004) | 5 of 20 (p=0.047) | 0 of 10 |
p-values are Fisher exact against the no-declarations row on the same endpoint.
Other things I found while isolating it:
- gemini-3.1 has the same behavior at a higher threshold. In September, with our own production instruction, the same name, 20 sessions per arm on the Developer API: 0 of 280 sessions were mispronounced at or below 1,854 bytes of declaration JSON, and 88 of 240 (37%) at or above 3,898 bytes.
- Declarations weigh far more than instruction text. On 3.1, a ~40,000-character system instruction did not cause it, while ~4 KB of declaration descriptions did.
- Description prose is what counts, not what the tools do. On 3.1, ten content-free declarations padded with neutral prose to the same size (8,159 bytes) were mispronounced as often as our ten real ones (13 and 7 of 20, vs 3 and 6 of 20). Cutting every description to one sentence (1,854 bytes) removed it (0 of 40). Long tool descriptions make it worse purely through their length.
- It needs both a longer instruction and the declarations. The same declarations with a bare 111-character instruction gave only 2 of 20 (API) and 1 of 20 (Vertex).
- Instruction length alone also triggers it. A ~15,000-character instruction with no declarations gave 5 of 20 on API 3.8.
- It is not about how often the name appears. Mentioning the name 8 times in the instruction padding did not make it worse.
- It is not first-utterance timing. In a fuller harness with the same model (API, ~1,729 bytes of declarations), delaying the greeting 4 seconds after
setup_completedid not help: 16 of 20 vs 8 of 20 with no delay. A second utterance of the name later in the same sessions was wrong about as often as the greeting: 14 of 20 vs 15 of 20, and in the same sessions. The mispronunciation follows the session's configuration. - It is size-dependent. In that harness on Vertex 3.8, ~470 bytes of declarations gave 1 of 20 and ~940 bytes gave 3 of 20, both close to the 0 of 20 control. ~1,729 and ~2,045 bytes each gave 9 of 20.
How it was graded. Each WAV went to a Gemini audio model 3 times. The model was asked to write the names it hears as they sound, without being told the spelling, and the majority read decided. I checked a sample of clips by ear, including ones the grader marked correct. The bundle has 3 mispronounced and 3 correct examples.
Possibly related: the forum reports that 3.8 Live does not follow language and accent settings, e.g. https://discuss.ai.google.dev/t/bug-gemini-3-8-live-api-overrides-language-code-ignores-system-prompt-accent-formatting/185304
To reproduce
Attached zip: the script below, 3 mispronounced and 3 correct model WAVs from these runs, and the results JSON for every arm.
pip install "google-genai>=2.24.0"
python case2_name_rendering_repro.py --surface api --name Juan --prompt-chars 1756 --runs 20 --out-dir out/control
python case2_name_rendering_repro.py --surface api --name Juan --prompt-chars 1756 --tool-bytes 1700 --runs 20 --out-dir out/tools
python case2_name_rendering_repro.py --surface vertex --project YOUR_PROJECT --name Juan --prompt-chars 1756 --tool-bytes 1700 --runs 20 --out-dir out/v_tools
Add --grade --good-re "^(h?w|wh)ah?n$" --ignore-re "^(d[eu]h?v|dev)$" to sound-grade automatically, or just listen to the WAVs in --out-dir.
case2_name_rendering_repro.py
"""Minimal reproduction: gemini-3.8-live renders a short Spanish first name with an English onset in
a scripted greeting when ~1.7 KB of unused function declarations are added to a ~1,750-character
system instruction. The same instruction without the declarations says it correctly.
WHAT IT DOES
1. Opens a Live session (AUDIO out, output transcription on) with a short fictitious system
instruction: Hannah, a phone assistant for a real-estate agent named NAME.
2. Condition (flags combine; none = control):
--tool-bytes N neutral filler function declarations (one STRING parameter each,
~157 bytes each) until their JSON reaches N bytes; never called
--prompt-chars N neutral padding appended to the system instruction up to N chars
--name-mentions K K neutral sentences naming the agent, spread through that padding
3. Triggers the greeting with one client_content message (role="user", turn_complete=True)
asking the model to say a fixed greeting word for word. The greeting says NAME three times.
4. Saves each session's model audio (24 kHz WAV) and output_transcription to --out-dir.
5. Optional --grade: sends each WAV 3 times to a Gemini text+audio model that lists every first
name it hears, written as it SOUNDS (it is never told the spelling). A read is GOOD only if
every listed name matches --good-re; any other name is BAD; no name is ABSENT. Majority of 3.
EXPECTED NAME is pronounced the same way in every condition (as it is in the control).
ACTUAL measured with THIS script (2026-10-12, one Spanish first name, sound-graded, majority of 3;
output_transcription showed the correct spelling throughout). Sessions with a bad rendering:
--prompt-chars 1756 (no tools) API 3.8 0/20 Vertex 3.8 0/20 API 3.1 0/10
--prompt-chars 1756 --tool-bytes 1700 (1729 B) API 3.8 10/20 Vertex 3.8 5/20 API 3.1 0/10
The same declarations with the bare 111-char instruction gave only 2/20 (API) and 1/20
(Vertex): the effect needs both a longer system instruction and the declarations.
A ~15,000-character instruction alone gave 5/20 on API 3.8.
A bad rendering keeps the name's letters but swaps the Spanish onset for an English one
(in the grader's syllables, a J-, D- or Y- onset, or the nearest English name).
USAGE
pip install "google-genai>=2.24.0"
gcloud auth application-default login # for --surface vertex
set GEMINI_API_KEY=... # for --surface api and for --grade
python case2_name_rendering_repro.py --surface api --name NAME --prompt-chars 1756 --runs 20 --out-dir out/a_p1 # control
python case2_name_rendering_repro.py --surface api --name NAME --prompt-chars 1756 --tool-bytes 1700 --runs 20 --out-dir out/a_p2
python case2_name_rendering_repro.py --surface vertex --project YOUR_PROJECT --name NAME --prompt-chars 1756 --tool-bytes 1700 --runs 20 --out-dir out/v_p2
add: --grade --good-re "<regex>" to sound-grade; <regex> matches the CORRECT sounded-out form
of NAME as the grader writes it (simple English syllables, stressed syllable in capitals)
--name is required: the name under test is supplied at run time.
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import re
import time
import wave
from collections import Counter
from pathlib import Path
from google import genai
from google.genai import types
SYSTEM = ("You are Hannah, a friendly phone assistant for {name}, a real-estate agent. "
"Keep replies to one or two sentences.")
GREETING = ("Hi, this is Hannah, {name}'s assistant. {name} can't come to the phone right now, but I can "
"take a message. Would you like {name} to call you back?")
CUE = ("The caller just connected and has not spoken yet. Say this greeting now, exactly, word for word, "
"then stop and listen: \"{greeting}\"")
PADDING_SENTENCES = [
"Background notes: the office keeps ordinary business hours, there is visitor parking by the lobby, "
"the team shares one calendar, and paperwork is filed electronically.",
"These notes change nothing about how you speak with callers.",
]
# --name-mentions: neutral sentences that mention the agent's name, used in turn.
NAME_LINES = [
"{name} works with buyers and sellers across the area.",
"Messages for {name} are reviewed at the end of each day.",
"{name} returns calls as soon as possible.",
"Callers often ask for {name} directly.",
"{name} keeps the shared calendar up to date.",
]
PHON_Q = ("List every person's first name spoken in this audio, except 'Hannah', in the order spoken. "
"Write each one only as it SOUNDS, in simple English syllables with the stressed syllable in "
"capitals (for example: ka-REE-na). One per line, nothing else. If there are none, write NONE.")
def filler_tools(target_bytes: int) -> tuple[list[types.Tool], int, int]:
decls, total = [], 0
while total < target_bytes:
d = types.FunctionDeclaration(
name=f"record_caller_detail_{len(decls) + 1}", description="Record one caller detail.",
parameters=types.Schema(type="OBJECT", properties={"value": types.Schema(type="STRING")}))
decls.append(d)
total += len(json.dumps(d.model_dump(exclude_none=True), ensure_ascii=False))
return ([types.Tool(function_declarations=decls)] if decls else []), total, len(decls)
def system_instruction(name: str, prompt_chars: int, mentions: int = 0) -> str:
"""SYSTEM, then neutral padding up to prompt_chars. With mentions=K, K neutral sentences that
name the agent are spread evenly through the padding (they are never truncated)."""
base = SYSTEM.format(name=name)
if not prompt_chars and not mentions:
return base
named = [NAME_LINES[i % len(NAME_LINES)].format(name=name) for i in range(mentions)]
room = max(0, prompt_chars - len(base) - 2)
neutral, size, i = [], sum(len(s) + 1 for s in named), 0
while size < room:
s = PADDING_SENTENCES[i % len(PADDING_SENTENCES)]
neutral.append(s)
size += len(s) + 1
i += 1
pieces = list(neutral)
step = len(neutral) / (len(named) + 1) if named else 0
for k, s in reversed(list(enumerate(named, 1))): # insert from the back so earlier slots stay put
pieces.insert(round(k * step), s)
pad = " ".join(pieces)
if neutral: # the last piece is always neutral, so this cut never touches a named sentence
pad = pad[:room]
return base + "\n\n" + pad
def config(system: str, tools: list[types.Tool]) -> types.LiveConnectConfig:
return types.LiveConnectConfig(
response_modalities=["AUDIO"],
system_instruction=system,
tools=tools or None,
temperature=0.7,
speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Leda"))),
input_audio_transcription=types.AudioTranscriptionConfig(),
output_audio_transcription=types.AudioTranscriptionConfig(),
)
async def run_session(client: genai.Client, model: str, cfg: types.LiveConnectConfig, cue: str,
wav_path: Path) -> dict:
audio, said, session_id = bytearray(), [], None
started = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
async with client.aio.live.connect(model=model, config=cfg) as session:
done = asyncio.Event()
async def receive() -> None:
nonlocal session_id
while not done.is_set():
async for msg in session.receive():
if msg.setup_complete and msg.setup_complete.session_id:
session_id = msg.setup_complete.session_id
sc = msg.server_content
if not sc:
continue
if sc.model_turn:
for p in sc.model_turn.parts or []:
if p.inline_data and p.inline_data.data:
audio.extend(p.inline_data.data)
if sc.output_transcription and sc.output_transcription.text:
said.append(sc.output_transcription.text)
if sc.turn_complete:
done.set()
break
receiver = asyncio.create_task(receive())
await session.send_client_content(turns=[types.Content(role="user", parts=[types.Part(text=cue)])],
turn_complete=True)
timed_out = False
try:
await asyncio.wait_for(done.wait(), timeout=20)
except asyncio.TimeoutError:
timed_out = True
receiver.cancel()
with wave.open(str(wav_path), "wb") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(24000)
w.writeframes(bytes(audio))
return {"started_utc": started, "session_id": session_id, "wav": wav_path.name,
"audio_s": round(len(audio) / 48000, 1), "output_transcription": "".join(said).strip(),
"timed_out": timed_out}
def classify(read: list[str], good_re: str, ignore_re: str) -> str:
names = [n for n in read if n.upper() != "NONE" and not (ignore_re and re.search(ignore_re, n, re.I))]
if not names:
return "ABSENT"
return "GOOD" if all(re.search(good_re, n, re.I) for n in names) else "BAD"
async def grade(gclient: genai.Client, gmodel: str, wav_path: Path, good_re: str, ignore_re: str) -> dict:
data, reads = wav_path.read_bytes(), []
for _ in range(3):
for attempt in range(4):
try:
r = await gclient.aio.models.generate_content(
model=gmodel, contents=[types.Part.from_bytes(data=data, mime_type="audio/wav"), PHON_Q])
break
except Exception as e: # noqa: BLE001 (retry only "model overloaded")
if "503" not in str(e) or attempt == 3:
raise
await asyncio.sleep(20 * (attempt + 1))
reads.append([ln.strip() for ln in (r.text or "").splitlines() if ln.strip()])
labels = [classify(rd, good_re, ignore_re) for rd in reads]
top, n = Counter(labels).most_common(1)[0]
return {"reads": [" / ".join(rd) for rd in reads], "labels": labels,
"majority": top if n >= 2 else "NO_MAJORITY", "split": len(set(labels)) > 1}
async def main() -> None:
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--surface", choices=["vertex", "api"], required=True)
ap.add_argument("--model", default="gemini-3.8-live")
ap.add_argument("--project", default=os.getenv("GOOGLE_CLOUD_PROJECT"))
ap.add_argument("--location", default="us")
ap.add_argument("--name", required=True, help="the first name under test (run-time only)")
ap.add_argument("--tool-bytes", type=int, default=0)
ap.add_argument("--prompt-chars", type=int, default=0)
ap.add_argument("--name-mentions", type=int, default=0,
help="neutral sentences naming the agent, spread through the padding")
ap.add_argument("--runs", type=int, default=20)
ap.add_argument("--concurrency", type=int, default=4)
ap.add_argument("--out-dir", type=Path, required=True)
ap.add_argument("--grade", action="store_true", help="sound-grade each WAV (needs GEMINI_API_KEY)")
ap.add_argument("--grade-model", default="gemini-3.8-flash")
ap.add_argument("--good-re", default=None, help="--grade: regex for the CORRECT sounded-out form")
ap.add_argument("--ignore-re", default="", help="--grade: listed tokens that are not the name")
a = ap.parse_args()
if a.grade and not a.good_re:
ap.error("--grade needs --good-re")
if a.surface == "vertex":
if not a.project:
ap.error("--project (or GOOGLE_CLOUD_PROJECT) is required for --surface vertex")
client = genai.Client(vertexai=True, project=a.project, location=a.location)
else:
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
gclient = genai.Client(api_key=os.environ["GEMINI_API_KEY"]) if a.grade else None
system = system_instruction(a.name, a.prompt_chars, a.name_mentions)
tools, tool_bytes, tool_count = filler_tools(a.tool_bytes) if a.tool_bytes else ([], 0, 0)
cue = CUE.format(greeting=GREETING.format(name=a.name))
mentions = len(re.findall(rf"\b{re.escape(a.name)}\b", system))
parts = ([f"prompt-chars {len(system)}"] if a.prompt_chars else []) + \
([f"name x{mentions} in system"] if a.name_mentions else []) + \
([f"tool-bytes {tool_bytes} ({tool_count} declarations)"] if a.tool_bytes else [])
cond = " + ".join(parts) or "control"
label = f"{a.surface}:{a.model} {cond}"
a.out_dir.mkdir(parents=True, exist_ok=True)
sem = asyncio.Semaphore(a.concurrency)
async def one(n: int) -> dict:
async with sem:
try:
r = await run_session(client, a.model, config(system, tools), cue, a.out_dir / f"session_{n:02d}.wav")
except Exception as e: # noqa: BLE001
return {"run": n, "error": f"{type(e).__name__}: {str(e)[:300]}"}
if a.grade and r["audio_s"] > 0.5:
try:
r["grade"] = await grade(gclient, a.grade_model, a.out_dir / r["wav"], a.good_re, a.ignore_re)
except Exception as e: # noqa: BLE001
r["grade_error"] = f"{type(e).__name__}: {str(e)[:200]}"
r["run"] = n
return r
results = await asyncio.gather(*(one(n) for n in range(1, a.runs + 1)))
print(f"google-genai {genai.__version__} {label} system_instruction={len(system)} chars")
for r in results:
if "error" in r:
print(f" session {r['run']:2d}: ERROR {r['error']}")
continue
g = r.get("grade", {})
print(f" session {r['run']:2d}: {g.get('majority', '-'):11s} {g.get('reads', '')} "
f"audio={r['audio_s']}s{' TIMEOUT' if r['timed_out'] else ''} transcript: {r['output_transcription'][:90]}")
tally = Counter(r.get("grade", {}).get("majority", "UNGRADED") for r in results if "error" not in r)
errors = sum("error" in r for r in results)
splits = sum(r.get("grade", {}).get("split", False) for r in results)
print(f"\nSUMMARY {label}: BAD {tally['BAD']} / GOOD {tally['GOOD']} / ABSENT {tally['ABSENT']} / "
f"NO_MAJORITY {tally['NO_MAJORITY']} / UNGRADED {tally['UNGRADED']} (split votes {splits}, errors {errors})")
(a.out_dir / "results.json").write_text(json.dumps({
"surface": a.surface, "model": a.model, "condition": cond, "system_instruction_chars": len(system),
"name_mentions_in_system": mentions,
"tool_bytes": tool_bytes, "tool_count": tool_count, "google_genai": genai.__version__,
"grade_model": a.grade_model if a.grade else None, "good_re": a.good_re, "ignore_re": a.ignore_re,
"tally": dict(tally), "split_votes": splits, "errors": errors, "sessions": results},
indent=2, ensure_ascii=False), encoding="utf-8")
if __name__ == "__main__":
asyncio.run(main())
Questions
- Is there documentation, or a recommended design pattern, for how the length or number of function declarations (and of the system instruction) affects speech quality in Live sessions? The Live API best practices page recommends prompt chaining and a distinct system instruction per agent, but I couldn't find anything on declaration size or count, or on speech. Even if this behavior is expected, a documented budget or pattern would let us design around it.
- Related: shortening descriptions avoids the speech problem but may cost tool-selection accuracy. Is there guidance on description length that balances the two, or a recommended way to scope declarations per stage of a conversation?
Happy to run anything that helps isolate it.
- 主要言語
- Python
- スター
- 4k
- フォーク
- 1k
- 平均マージ
- 1日 6時間
- マージ済み PR(30日)
- 88
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
googleapis/python-genai のほかの issue
-
Test import module that doesn't exist対応中かも @Venkaiahbabuneelam が今日担当しました。 オープンpriority: p2 status:awaiting user response type: bug
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
googleapis/python-genai#3122 · コメント 3 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
FunctionDeclaration.from_callable drops enum and properties from Optional[...] parameters対応中かも @Venkaiahbabuneelam が 1 日前に担当しました。 オープンpriority: p2 type: bug
難易度 2/5 1〜3時間 初心者へのやさしさ 67/100
googleapis/python-genai#3105 · コメント 1 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
Curated history keeps half a user turn when the model turn is invalid対応中かも @Venkaiahbabuneelam が 9 日前に担当しました。 オープンpriority: p2 type: bug
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
googleapis/python-genai#3051 · コメント 3 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
[Bug]: Unsubscripted typing.List and typing.Dict crash convert_if_exist_pydantic_model in AFC対応中かも @Venkaiahbabuneelam が 9 日前に担当しました。 オープンpriority: p2 type: bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
googleapis/python-genai#3044 · コメント 1 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
Name collision in google.genai.interactions: triggers.Interaction shadows response model Interaction in static type checkers対応中かも @Venkaiahbabuneelam が 16 日前に担当しました。 オープンpriority: p2 type: bug
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
googleapis/python-genai#3013 · コメント 1 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
googleapis/python-genai の issue をすべて見る
似ている issue
-
customer-reported question
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
Azure/azure-sdk-for-python#49548 · コメント 1 件 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 88/100
btclib-org/btclib-wallet#409 ·
メンテナーはふだん 1 日以内に返信
-
Phase 2 PSA Capability & Career Profile
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
bcgov/digital-journeys#2310 ·
-
bug good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
fastmachinelearning/hls4ml#1582 ·
メンテナーはふだん 3 日以内に返信
-
Sort modelsオープンenhancement good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
メンテナーはふだん 1 日以内に返信