Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions livekit-agents/livekit/agents/tts/_provider_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,9 +634,12 @@ def sentence_tokenizer(provider: str, *, expressive: bool) -> tokenize.SentenceT
_EXPR_CLOSE_RE = re.compile(r"</expr\s*>")
# self-closing markers only (the trailing / is required)
_EXPR_SELF_RE = re.compile(r"<expr\b([^>]*?)/\s*>")
# a wrapping marker (prosody/spell) and its span; non-greedy, instructed not to nest
# a wrapping marker (prosody/spell) and its span; non-greedy, instructed not to nest.
# the opening tag must not itself be self-closing, or a self-closing prosody point
# control ahead of a later wrapping marker gets mistaken for that marker's opener,
# swallowing everything up to the next </expr> into its span.
_EXPR_WRAP_RE = re.compile(
r'<expr\b(?=[^>]*type="(?:prosody|spell)")([^>]*?)>(.*?)</expr\s*>', re.DOTALL
r'<expr\b(?=[^>]*type="(?:prosody|spell)")([^>]*?)(?<!/)>(.*?)</expr\s*>', re.DOTALL
)
# a non-wrapping type the LLM forgot to self-close (normalize_markup fixes these)
_EXPR_UNCLOSED_RE = re.compile(
Expand Down
12 changes: 12 additions & 0 deletions tests/test_expr_markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ def test_convert_expr_cartesia_prosody_unknown_label_unwraps() -> None:
assert convert_markup("cartesia", text) == "keep it secret"


def test_convert_expr_cartesia_self_closing_prosody_before_spell() -> None:
# a self-closing point control ahead of a later wrapping marker must not be
# mistaken for that marker's opening tag — each converts independently
text = (
'Say it slow: <expr type="prosody" label="slow"/> '
'and now spell it: <expr type="spell">A7X9</expr> done.'
)
assert convert_markup("cartesia", text) == (
'Say it slow: <speed ratio="0.85"/> and now spell it: <spell>A7X9</spell> done.'
)


def test_convert_stray_expr_never_reaches_tts() -> None:
# an unpaired prosody open/close (e.g. split across stream chunks) is dropped,
# keeping the words
Expand Down