Skip to content

fix(voice): strip break and markup tags before adding say() text to chat_ctx (#4802) - #7251

Open
rrfunde wants to merge 9 commits into
livekit:mainfrom
rrfunde:fix/strip-say-break-tags-from-chat-ctx
Open

fix(voice): strip break and markup tags before adding say() text to chat_ctx (#4802)#7251
rrfunde wants to merge 9 commits into
livekit:mainfrom
rrfunde:fix/strip-say-break-tags-from-chat-ctx

Conversation

@rrfunde

@rrfunde rrfunde commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #4802

Summary

When using session.say() with text containing pause tags like <break time="1s"/> or provider markup tags, the tags are synthesized as pauses by the TTS engine, but previously leaked directly into the ChatContext assistant message. Storing these raw tags in chat history caused subsequent LLM turns to be few-shotted into emitting unsupported markup tags.

Changes

  • In AgentActivity._tts_task_impl, sanitize forwarded_text with strip_all_markup before adding it as an assistant message to chat_ctx.
  • Trims surrounding whitespace and ensures pure-pause calls (containing only break tags) do not produce empty messages in context.
  • Added hermetic unit test in tests/test_say_break_tag.py.

…hat_ctx (livekit#4802)

Prevent <break> SSML and provider markup tags from leaking into ChatContext assistant messages when using say().
@rrfunde
rrfunde requested a review from a team as a code owner September 12, 2026 11:32
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
πŸ“ Code Review βœ… Completed 2026-09-14T02:12:01.966598Z 9db9405 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with πŸ‘€ while any review is running, comments if it has suggestions, and reacts with πŸ‘ once all reviews finish with no findings.

devin-ai-integration[bot]

This comment was marked as resolved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d568d1e63

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

)
speech_handle._item_added([msg])
self._session._conversation_item_added(msg)
clean_text = strip_all_markup(forwarded_text).strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve word boundaries around stripped break tags

When valid SSML places a pause directly between words, such as session.say('Hello<break time="1s"/>world'), strip_all_markup() removes the self-closing tag without inserting a separator, so the stored assistant message becomes Helloworld even though the user heard β€œHello … world.” This corrupts the conversation history for a common tag layout; preserve a word boundary when removing a break between non-whitespace characters.

Useful? React with πŸ‘Β / πŸ‘Ž.

)
speech_handle._item_added([msg])
self._session._conversation_item_added(msg)
clean_text = strip_all_markup(forwarded_text).strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip the other SSML tags accepted by TTS providers

For providers supporting broader SSML, this sanitizer still leaves markup in history because strip_all_markup() only recognizes the expressive-provider tag union. For example, ElevenLabs explicitly accepts <phoneme> when SSML parsing is enabled (livekit-plugins/livekit-plugins-elevenlabs/livekit/plugins/elevenlabs/tts.py:513), but session.say('<phoneme alphabet="ipa" ph="...">tomato</phoneme>') stores that entire tag unchanged and therefore retains the same few-shot leakage this change is intended to prevent. The cleanup needs to cover supported SSML such as phoneme tags, preferably according to the active provider.

Useful? React with πŸ‘Β / πŸ‘Ž.

@rrfunde

rrfunde commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback:

  • Introduced strip_chat_markup() which replaces <break> tags with a space before collapsing extra spaces, preserving word boundaries when tags sit between words (e.g. Hello<break time="1s"/>world -> Hello world).
  • Unwraps SSML wrapping tags accepted by bundled providers (<phoneme>, <prosody>, <say-as>, <emphasis>, <sub>, <voice>, <lang>, etc.) while retaining their spoken inner text, removing framing tags, and stripping provider expressive markup.
  • Added test coverage in tests/test_say_break_tag.py for word boundaries and SSML tags.

devin-ai-integration[bot]

This comment was marked as resolved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f37313ad5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


# Unwrap SSML wrapping tags to keep inner text
while True:
unwrapped = _SSML_WRAPPING_RE.sub(r"\2", text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve boundaries between structural SSML elements

When session.say() receives valid adjacent paragraph or sentence elements, such as <p>Hello</p><p>world</p>, the new wrapper substitution replaces each element with only its inner text and stores Helloworld in the chat context even though the TTS renders a structural boundary. Handle <p> and <s> separately by inserting a separator rather than treating them like inline wrappers.

Useful? React with πŸ‘Β / πŸ‘Ž.


_SSML_BREAK_RE = re.compile(r"<\s*/?\s*break\b[^>]*\/?>", re.IGNORECASE)
_SSML_WRAPPING_RE = re.compile(
r"<\s*(?P<tag>phoneme|sub|say-as|prosody|emphasis|voice|lang|speak|p|s|w|audio|mstts:[a-zA-Z0-9_-]+)\b[^>]*>(.*?)</\s*(?P=tag)\s*>",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip incomplete SSML wrappers after interruption

When SSML speech is interrupted before its closing tag is forwarded, the synchronized transcript can end with text such as <phoneme alphabet="ipa" ph="...">tomato. The new regex requires a matching closing tag, while the standalone pass excludes phoneme, prosody, say-as, and most other wrappers, so the opening markup is still stored and can few-shot later LLM turns. After balanced unwrapping, strip lone recognized SSML delimiters as well.

Useful? React with πŸ‘Β / πŸ‘Ž.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 1 new potential issue.

1 flag not posted on this PR by your GitHub settings β€” view it in Devin Review. (Configure)

Devin Review


# Replace structural SSML tags (<p>, <s>) with inner text + space separator
while True:
replaced = _SSML_STRUCTURAL_RE.sub(r"\2 ", text)

@devin-ai-integration devin-ai-integration Bot Sep 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Structural tags corrupt trailing punctuation

When punctuation follows a closed <p> or <s>, strip_chat_markup inserts a space before it. Chat history changes Hello! into Hello !, few-shotting malformed punctuation into later responses.

Learn more

Structural tags need a separator only when removing the boundary would concatenate surrounding words. The unconditional trailing space also separates punctuation and contractions from their preceding text. This sanitized value becomes the assistant message in the TTS task, so the modified punctuation persists in conversation history.

Example: strip_chat_markup("<p>Hello</p>!") returns "Hello !"; the expected plain text is "Hello!". Adjacent blocks such as <p>Hello</p><p>world</p> still require "Hello world".

Recommended fix: Preserve a boundary conditionally. Add whitespace only when the next structural block or surrounding plain text would otherwise join two word characters, while leaving punctuation attached.

Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9cac0d13e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

re.IGNORECASE | re.DOTALL,
)
_SSML_WRAPPING_RE = re.compile(
r"<\s*(?P<tag>phoneme|sub|say-as|prosody|emphasis|voice|lang|speak|w|audio|mstts:[a-zA-Z0-9_-]+)\b[^>]*>(.*?)</\s*(?P=tag)\s*>",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip provider-specific SSML tags before storing history

When the AWS TTS plugin is configured with text_type="ssml", _run() forwards the session text unchanged as Polly SSML (livekit-plugins/livekit-plugins-aws/livekit/plugins/aws/tts.py:163-170). This new fixed whitelist recognizes only common unprefixed tags and mstts:*, so a supported call using an Amazon tag, such as <amazon:effect name="drc">Breaking news</amazon:effect>, is still stored verbatim in chat_ctx, preserving the markup leakage this change is intended to eliminate. The newly introduced whitelist is fresh evidence that the previously reported broad-SSML issue remains for provider-specific dialects.

Useful? React with πŸ‘Β / πŸ‘Ž.

@rrfunde

rrfunde commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Fixed:

  1. Ensured strip_chat_markup trims whitespace-only text on the early-return path.
  2. Handled structural SSML tags (<p>, <s>) separately by inserting spaces so words across paragraphs/sentences do not merge.
  3. Added cleanup for incomplete opening SSML tags left by interruptions.

@rrfunde

rrfunde commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review feedback: generalized SSML tag pattern to include namespaced provider tags ([a-zA-Z][a-zA-Z0-9_-]*:[a-zA-Z0-9_-]+), unwrapping provider dialects such as Amazon Polly (<amazon:effect>, <amazon:domain>), Azure Speech (<mstts:...>), and stripping standalone tags (<amazon:breath/>). Added unit tests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7798379c73

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

re.IGNORECASE | re.DOTALL,
)
_SSML_STANDALONE_RE = re.compile(
rf"<\s*/?\s*(?:speak|p|s|mark|{_SSML_TAG_PATTERN})\b[^>]*\/?>",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve literal markup when SSML is not enabled

When say() is used with a plain-text TTS configuration or text-only output, this pattern still treats ordinary HTML/XML examples as SSML because strip_chat_markup() receives no provider or SSML-mode information. For example, session.say("Use <p> and </p> tags") is forwarded to the user with the tags intact but stored as Use and tags, causing subsequent LLM turns to see content different from what was delivered; only strip tags that the active output actually interprets as markup.

Useful? React with πŸ‘Β / πŸ‘Ž.

r"[a-zA-Z][a-zA-Z0-9_-]*:[a-zA-Z0-9_-]+"
)
_SSML_WRAPPING_RE = re.compile(
rf"<\s*(?P<tag>{_SSML_TAG_PATTERN})\b[^>]*>(.*?)</\s*(?P=tag)\s*>",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle closing angles inside quoted SSML attributes

For valid SSML whose quoted attribute contains >, the opening-tag match ends at that character rather than at the actual delimiter. For example, <sub alias="2 > 1">comparison</sub> is sanitized to 1">comparison instead of comparison, leaving attribute fragments in chat history; parse quoted attributes without treating their angle brackets as the end of the tag.

Useful? React with πŸ‘Β / πŸ‘Ž.

@rrfunde

rrfunde commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review feedback:

  1. Handled closing angles inside quoted SSML attributes (e.g. <sub alias="2 > 1">comparison</sub>) by matching quoted string literals within attribute patterns.
  2. Preserved literal HTML/XML markup (e.g. <p>, <s>) when SSML is not enabled (plain-text TTS configuration or text-only output), only stripping SSML tags when SSML mode is active or text contains root <speak> tags.
  3. Added unit tests covering quoted angle brackets, plain-text HTML preservation, and text-only sessions.

devin-ai-integration[bot]

This comment was marked as resolved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36fc2e509d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1107 to +1108
if provider in ("azure", "cartesia"):
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the provider names exposed by TTS plugins

When audio output uses Azure TTS, this check never enables SSML handling because the plugin exposes provider == "Azure TTS" (livekit-plugins/livekit-plugins-azure/livekit/plugins/azure/tts.py:217) rather than "azure", and its options contain none of the preceding SSML flags. Azure always embeds the supplied text into an SSML <speak><voice> document (tts.py:262-293), so input such as <p>Hello</p><p>world</p> is interpreted structurally during synthesis but remains verbatim in chat history. Normalize the exposed provider value or use an explicit capability instead of comparing against these lowercase internal keys.

Useful? React with πŸ‘Β / πŸ‘Ž.

@rrfunde

rrfunde commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review feedback:

  • Normalized the TTS provider string by lowercasing and trimming, and inspected the class's module name (e.g. recognizing "Azure TTS" and livekit.plugins.azure) so Azure TTS and similar providers correctly enable SSML unwrapping for structural tags.
  • Added unit test verifying provider = "Azure TTS" enables SSML tag unwrapping.

devin-ai-integration[bot]

This comment was marked as resolved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e78d360700

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


# Unwrap inline SSML wrapping tags to keep inner text
while True:
unwrapped = _SSML_WRAPPING_RE.sub(r"\2", text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Store the spoken alias for <sub> elements

When SSML contains <sub alias="World Wide Web Consortium">W3C</sub>, the TTS speaks the alias, but this generic unwrapping stores W3C in the assistant history. This makes subsequent turns reason over text different from what the user heard; handle <sub> separately and store its decoded alias value rather than its inner text.

Useful? React with πŸ‘Β / πŸ‘Ž.

Comment on lines +1090 to +1091
if any(tag in text_lower for tag in ("<phoneme", "<prosody", "<say-as")):
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate SSML stripping on the actual output mode

For text-only output or a plain-text TTS, prose such as Explain <phoneme alphabet="ipa">tomato</phoneme> is delivered with the literal tags, but this content-based check declares it SSML and removes them from chat history. The new auto-detection heuristic is fresh evidence that the previously reported plain-markup mismatch remains for these tags; SSML mode should come from the active output configuration rather than from the text being sanitized.

Useful? React with πŸ‘Β / πŸ‘Ž.

Comment on lines +1096 to +1098
opts = getattr(tts, "_opts", None)
if opts is not None:
if getattr(opts, "text_type", None) == "ssml":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Inspect wrapped TTS instances for SSML mode

When an SSML-enabled engine is supplied through tts.FallbackAdapter or an explicit tts.StreamAdapter, this examines the adapter's _opts rather than the wrapped engine's options. For example, FallbackAdapter([google.TTS(enable_ssml=True, use_streaming=False)]) interprets <p>Hello</p><p>world</p> as SSML, while this function sees neither enable_ssml nor a recognized provider and leaves the tags in history; delegate an explicit SSML capability through these standard adapters.

Useful? React with πŸ‘Β / πŸ‘Ž.

@rrfunde

rrfunde commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review feedback:

  1. Replaced content-based auto-detection heuristics in _is_ssml_enabled() so prose containing literal markup (e.g. <phoneme ...>, <p>, <speaker>) is preserved when SSML is not enabled.
  2. In strip_chat_markup(), handled <sub alias="...">...</sub> tags by substituting their decoded alias attribute (what was spoken) into assistant chat history.
  3. Updated _is_ssml_enabled() to recursively inspect wrapped TTS instances in adapters (FallbackAdapter, StreamAdapter).
  4. Matched canonical Azure Speech and Cartesia provider names ({"azure", "azure tts", "cartesia"}) to prevent Azure OpenAI endpoint hostnames from being misclassified as SSML.
  5. Added unit tests for <sub> alias substitution, wrapped adapters, Azure OpenAI preservation, and literal markup preservation.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 3 new potential issues.

2 flags not posted on this PR by your GitHub settings β€” view them in Devin Review. (Configure)

Devin Review

Comment on lines +3332 to +3334
tts=self.tts
if (self._session.output.audio_enabled and self.tts is not None)
else None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Supplied-audio transcript markup corruption

strip_chat_markup treats supplied-audio text as SSML whenever the session's unused TTS supports it. Literal <p> and <s> content disappears from assistant history although supplied audio bypasses synthesis.

Learn more

A say() call can provide its own audio plus text used for transcription and chat history. That path skips perform_tts_inference, but cleanup currently consults the session TTS anyway. An SSML-capable configured TTS therefore changes text it never synthesized.

Example: A session configured with Azure calls say("Use <p> and </p> tags", audio=recording). The recording plays directly, but the stored assistant message becomes "Use and tags" instead of preserving the supplied transcript.

Recommended fix: Pass the TTS to strip_chat_markup only when audio is None and synthesis actually uses the configured TTS.

Suggested change
tts=self.tts
if (self._session.output.audio_enabled and self.tts is not None)
else None,
tts=self.tts
if (
audio is None
and self._session.output.audio_enabled
and self.tts is not None
)
else None,
Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +1170 to +1177
# Provider-specific namespaced tags (e.g. Amazon Polly, Azure) are always speech markup
while True:
unwrapped = _SSML_NAMESPACED_WRAPPING_RE.sub(r"\2", text)
if unwrapped == text:
break
text = unwrapped
text = _SSML_NAMESPACED_STANDALONE_RE.sub(" ", text)
text = _SSML_NAMESPACED_INCOMPLETE_RE.sub("", text)

@devin-ai-integration devin-ai-integration Bot Sep 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Literal namespaced XML disappears

strip_chat_markup removes every namespaced XML element even when SSML is disabled. Spoken XML examples lose tags and standalone elements in assistant history.

Learn more

Namespaced syntax is not sufficient to identify speech markup. The pattern accepts arbitrary names such as foo:bar, and these substitutions run before _is_ssml_enabled. Standard tags receive an SSML gate, but arbitrary namespaced tags do not.

Example: session.say("Use <foo:bar>value</foo:bar> in the document") stores "Use value in the document", even in a text-only session. The expected history preserves the XML example because no SSML mode was selected.

Recommended fix: Gate namespaced cleanup on confirmed SSML input, or restrict unconditional cleanup to a documented set of provider speech namespaces with explicit signaling for ambiguous text.

Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +1147 to +1149
instances = getattr(curr, "_tts_instances", None)
if instances is not None and isinstance(instances, (list, tuple)):
to_check.extend(instances)

@devin-ai-integration devin-ai-integration Bot Sep 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Inactive fallback enables SSML stripping

_is_ssml_enabled returns true when any fallback TTS supports SSML, even if a plain-text provider serves the request. Literal standard tags then vanish from assistant history.

Learn more

A fallback adapter selects one provider per request and records the provider that actually served it as _active_instance. Scanning every configured instance treats their input formats as a union. That does not describe the provider that produced the audio.

Example: A fallback list contains OpenAI first and Azure second. OpenAI successfully speaks "Use <p> and </p> tags" as plain text, but Azure's presence enables SSML cleanup and stores "Use and tags".

Recommended fix: Determine cleanup from the provider that served the completed request. For fallback adapters, inspect _active_instance after synthesis rather than enabling every member's input dialect.

Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 48804e256c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1147 to +1149
instances = getattr(curr, "_tts_instances", None)
if instances is not None and isinstance(instances, (list, tuple)):
to_check.extend(instances)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the fallback engine that actually synthesized the speech

When a FallbackAdapter mixes engines with different SSML modesβ€”for example, a Google primary with enable_ssml=False and an SSML-enabled fallbackβ€”scanning every _tts_instances entry returns true even when the plain-text primary produced the audio. Standard tags are then removed from history despite being delivered as literal text; the reverse ordering also misrepresents output after failover. The newly added traversal of all fallback instances is fresh evidence beyond the earlier wrapped-adapter report; determine the mode from the engine that served this request rather than any possible fallback.

Useful? React with πŸ‘Β / πŸ‘Ž.

Comment on lines +1215 to +1217
# Collapse any introduced horizontal whitespace runs
text = re.sub(r"[^\S\r\n]+", " ", text)
return text.strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Decode retained XML character references

When valid SSML contains escaped text such as <speak>AT&amp;T</speak> or an escaped <sub> alias, the TTS XML parser speaks the decoded characters, but this function returns AT&amp;T unchanged after removing the wrappers. That leaves entity markup in assistant history and makes later turns see text different from what was spoken; decode XML character and numeric references in retained SSML text and aliases before returning it.

Useful? React with πŸ‘Β / πŸ‘Ž.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 9db9405: decoded XML character and numeric references (html.unescape) in retained SSML text and substituted aliases when SSML is active, while keeping XML references untouched when SSML is disabled. Added unit tests.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

2 flags not posted on this PR by your GitHub settings β€” view them in Devin Review. (Configure)

Devin Review


# Decode XML character and numeric references in retained SSML text and aliases
if is_ssml or had_namespaced:
text = html.unescape(text)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 C1 character references change value

For C1 numeric references, html.unescape applies HTML’s Windows-1252 remapping instead of XML decoding. Chat history then records different characters from the SSML text sent to synthesis.

Learn more

SSML uses XML character-reference rules, while html.unescape implements HTML5 parsing. HTML5 remaps numeric references from U+0080 through U+009F using the Windows-1252 table. XML preserves those valid code points, so the sanitizer can no longer represent the text passed to the TTS provider.

Example: strip_chat_markup("<speak>&#x80;</speak>") returns €. An XML processor resolves &#x80; to U+0080 instead, so the stored assistant message differs from the synthesized input.

Recommended fix: Decode only XML’s five predefined named entities and numeric references. Validate numeric code points against XML’s allowed character ranges, and preserve or reject invalid references consistently.

Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9db9405b55

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3332 to +3334
tts=self.tts
if (self._session.output.audio_enabled and self.tts is not None)
else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore configured TTS when supplied audio bypasses synthesis

When session.say(..., audio=frames) is used while an SSML-capable TTS such as Azure is configured, _tts_task_impl takes the audio is not None branch and never sends the text to that TTS, but this still passes self.tts to the sanitizer. Literal transcript text such as Use <p> and </p> is consequently rewritten in chat history according to a model that did not synthesize the supplied audio; only infer SSML mode from self.tts when audio is None.

Useful? React with πŸ‘Β / πŸ‘Ž.

text = html.unescape(text)

# Collapse any introduced horizontal whitespace runs
text = re.sub(r"[^\S\r\n]+", " ", text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve whitespace in non-SSML text

When SSML is disabled but the text contains any < character, the fast return is skipped and this unconditional normalization collapses user-authored tabs and repeated spaces even if no markup was removed. For example, text-only session.say("Use <p> and preserve\tspacing") is delivered in its original form but stored as Use <p> and preserve spacing; limit normalization to whitespace introduced by tag substitutions so plain-text history remains faithful.

Useful? React with πŸ‘Β / πŸ‘Ž.

Comment on lines +1216 to +1217
# Remove standalone / framing tags
text = _SSML_STANDALONE_RE.sub(" ", text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove XML comments from SSML history

Valid SSML may contain XML comments, which synthesis ignores, but the cleanup only removes recognized element delimiters. Thus <speak>Hello<!-- internal directive -->world</speak> is stored as Hello<!-- internal directive -->world, retaining unheard markup and allowing it to influence later LLM turns; strip XML comments (and other non-spoken XML framing) while SSML mode is active.

Useful? React with πŸ‘Β / πŸ‘Ž.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"<break" tag leaks into conversation message when used in .say

1 participant