feat(xai-tts): wire text_normalization parameter

xAI TTS supports a text_normalization boolean that normalizes
written-form text (numbers, abbreviations, symbols) into spoken-form
before synthesis. This parameter was not being sent, leaving users
with literal number/symbol pronunciation.

- Add DEFAULT_XAI_TEXT_NORMALIZATION_DEFAULT = False
- Read tts.xai.text_normalization from config (via _xai_bool_config)
- Attach text_normalization: true to the POST payload when enabled
- Omit the field entirely when unset or explicitly false (API default)
- Add 3 tests: default omission, enabled passthrough, explicit false
This commit is contained in:
Carlos Diosdado 2026-07-01 16:52:42 -06:00 committed by Teknium
parent 462b3cf994
commit d1e9344716
2 changed files with 87 additions and 0 deletions

View file

@ -580,3 +580,77 @@ def test_generate_xai_tts_provider_speed_overrides_global(tmp_path, monkeypatch)
)
assert captured["json"]["speed"] == 0.7
def test_generate_xai_tts_omits_text_normalization_by_default(tmp_path, monkeypatch):
"""text_normalization is not sent when unset (API default is False)."""
captured = {}
fake_response = Mock()
fake_response.content = b"mp3"
fake_response.raise_for_status.return_value = None
def fake_post(url, headers, json, timeout):
captured["json"] = json
return fake_response
monkeypatch.setenv("XAI_API_KEY", "test-xai-key")
monkeypatch.setattr("requests.post", fake_post)
_generate_xai_tts(
"Hello world.",
str(tmp_path / "out.mp3"),
{"xai": {"voice_id": "ara", "language": "en"}},
)
assert "text_normalization" not in captured["json"]
def test_generate_xai_tts_sends_text_normalization_when_enabled(tmp_path, monkeypatch):
"""tts.xai.text_normalization: true flows into the POST body."""
captured = {}
fake_response = Mock()
fake_response.content = b"mp3"
fake_response.raise_for_status.return_value = None
def fake_post(url, headers, json, timeout):
captured["json"] = json
return fake_response
monkeypatch.setenv("XAI_API_KEY", "test-xai-key")
monkeypatch.setattr("requests.post", fake_post)
_generate_xai_tts(
"Hello world.",
str(tmp_path / "out.mp3"),
{"xai": {"voice_id": "ara", "language": "en", "text_normalization": True}},
)
assert captured["json"]["text_normalization"] is True
def test_generate_xai_tts_omits_text_normalization_when_explicit_false(
tmp_path, monkeypatch
):
"""text_normalization: false is the API default; field is not sent."""
captured = {}
fake_response = Mock()
fake_response.content = b"mp3"
fake_response.raise_for_status.return_value = None
def fake_post(url, headers, json, timeout):
captured["json"] = json
return fake_response
monkeypatch.setenv("XAI_API_KEY", "test-xai-key")
monkeypatch.setattr("requests.post", fake_post)
_generate_xai_tts(
"Hello world.",
str(tmp_path / "out.mp3"),
{"xai": {"voice_id": "ara", "language": "en", "text_normalization": False}},
)
assert "text_normalization" not in captured["json"]

View file

@ -235,6 +235,10 @@ DEFAULT_XAI_SPEED_DEFAULT = 1.0
# xAI TTS `optimize_streaming_latency` accepts 0, 1, or 2; 0 (best quality) is
# the API default (omitted => default). Values >0 trade quality for time-to-first-audio.
DEFAULT_XAI_OPTIMIZE_STREAMING_LATENCY_DEFAULT = 0
# xAI TTS `text_normalization` is a boolean (default False). When enabled,
# the model normalizes written-form text (numbers, abbreviations, symbols)
# into spoken-form before generating audio.
DEFAULT_XAI_TEXT_NORMALIZATION_DEFAULT = False
DEFAULT_GEMINI_TTS_MODEL = "gemini-2.5-flash-preview-tts"
DEFAULT_GEMINI_TTS_VOICE = "Kore"
DEFAULT_GEMINI_TTS_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
@ -1594,6 +1598,12 @@ def _generate_xai_tts(text: str, output_path: str, tts_config: Dict[str, Any]) -
optimize_streaming_latency = None
if optimize_streaming_latency is not None:
optimize_streaming_latency = max(0, min(2, optimize_streaming_latency))
# ``tts.xai.text_normalization`` enables spoken-form normalization
# (numbers, abbreviations, symbols → words). Defaults to False.
text_normalization = _xai_bool_config(
xai_config.get("text_normalization"),
DEFAULT_XAI_TEXT_NORMALIZATION_DEFAULT,
)
if auto_speech_tags:
text = _apply_xai_auto_speech_tags(text)
if creds.get("provider") == "xai-oauth":
@ -1637,6 +1647,9 @@ def _generate_xai_tts(text: str, output_path: str, tts_config: Dict[str, Any]) -
and optimize_streaming_latency != DEFAULT_XAI_OPTIMIZE_STREAMING_LATENCY_DEFAULT
):
payload["optimize_streaming_latency"] = optimize_streaming_latency
# Only attach `text_normalization` when explicitly enabled (default is False).
if text_normalization:
payload["text_normalization"] = True
response = requests.post(
f"{base_url}/tts",