fix(elevenlabs): use alignment by default, normalizedAlignment only with pronunciation dicts

PR #4344 unconditionally switched to normalizedAlignment to fix garbled
words with pronunciation dictionaries (#4316). But normalizedAlignment
returns the post-normalized form of what was spoken - including
romanization of non-Latin scripts (Chinese rendered as pinyin), which
ends up in the LLM context and degrades subsequent turns.

Gate the switch on pronunciation_dictionary_locators being configured.
Adds a _select_alignment helper with preferred-with-fallback (both
fields are nullable per the API schema), used by both the WebSocket
and HTTP services. Tests cover dictionary mode, default mode, fallback
when preferred is missing or null, and HTTP field-name variants.
This commit is contained in:
Mark Backman
2026-05-05 14:32:29 -04:00
parent 735cd09c7e
commit 61a81ed87b
2 changed files with 164 additions and 12 deletions

View File

@@ -9,6 +9,7 @@
from typing import Any
from pipecat.services.elevenlabs.tts import (
_select_alignment,
_strip_utterance_leading_spaces,
calculate_word_times,
)
@@ -90,3 +91,112 @@ def test_elevenlabs_alignment_strips_only_utterance_leading_spaces():
assert first["chars"] == list("Hello")
assert subsequent["chars"] == list(" world")
def test_select_alignment_default_prefers_alignment():
msg = {
"alignment": _chunk("Hello"),
"normalizedAlignment": _chunk(" Hello"),
}
selected = _select_alignment(
msg,
normalized_key="normalizedAlignment",
alignment_key="alignment",
prefer_normalized=False,
)
assert selected is not None
assert selected["chars"] == list("Hello")
def test_select_alignment_dictionary_mode_prefers_normalized():
msg = {
"alignment": _chunk("Hello"),
"normalizedAlignment": _chunk(" Hello"),
}
selected = _select_alignment(
msg,
normalized_key="normalizedAlignment",
alignment_key="alignment",
prefer_normalized=True,
)
assert selected is not None
assert selected["chars"] == list(" Hello")
def test_select_alignment_falls_back_when_preferred_missing():
msg_default = {"normalizedAlignment": _chunk(" Hello")}
selected = _select_alignment(
msg_default,
normalized_key="normalizedAlignment",
alignment_key="alignment",
prefer_normalized=False,
)
assert selected is not None
assert selected["chars"] == list(" Hello")
msg_dict = {"alignment": _chunk("Hello")}
selected = _select_alignment(
msg_dict,
normalized_key="normalizedAlignment",
alignment_key="alignment",
prefer_normalized=True,
)
assert selected is not None
assert selected["chars"] == list("Hello")
def test_select_alignment_falls_back_when_preferred_null():
msg = {"alignment": None, "normalizedAlignment": _chunk(" Hello")}
selected = _select_alignment(
msg,
normalized_key="normalizedAlignment",
alignment_key="alignment",
prefer_normalized=False,
)
assert selected is not None
assert selected["chars"] == list(" Hello")
def test_select_alignment_returns_none_when_both_missing():
assert (
_select_alignment(
{},
normalized_key="normalizedAlignment",
alignment_key="alignment",
prefer_normalized=False,
)
is None
)
assert (
_select_alignment(
{"alignment": None, "normalizedAlignment": None},
normalized_key="normalizedAlignment",
alignment_key="alignment",
prefer_normalized=True,
)
is None
)
def test_select_alignment_works_with_http_field_names():
msg = {
"alignment": {"characters": list("Hi")},
"normalized_alignment": {"characters": list(" Hi")},
}
selected = _select_alignment(
msg,
normalized_key="normalized_alignment",
alignment_key="alignment",
prefer_normalized=False,
)
assert selected is not None
assert selected["characters"] == list("Hi")
selected = _select_alignment(
msg,
normalized_key="normalized_alignment",
alignment_key="alignment",
prefer_normalized=True,
)
assert selected is not None
assert selected["characters"] == list(" Hi")