text_segment_map

Segment-level mapping between original and TTS-transformed text.

class pipecat.utils.context.text_segment_map.TextSegment(original: str, tts: str, original_start: int, original_end: int)[source]

Bases: object

Immutable aligned chunk between original and TTS text.

Parameters:
  • original – Chunk of the user-facing / LLM text.

  • tts – Corresponding chunk in the TTS-transformed text.

  • original_start – Byte offset in original_text where this chunk begins.

  • original_end – Byte offset in original_text where this chunk ends.

original: str
tts: str
original_start: int
original_end: int
property is_transformed: bool

True when this segment cannot be tracked by proportional char advancement.

This holds either when the alphanumeric content differs between original and TTS sides, or when a replacement changed the segment’s word count (e.g. splitting "BODYPUMP" into "body pump", or letter-spacing an acronym like "API" into "A P I"). Word-splitting replacements can normalize to the same alphanumeric content on both sides, but the proportional advance still breaks: it would consume alnum chars from a single contiguous original token using word boundaries that only exist on the TTS side, landing mid-word instead of at a real boundary.

property tts_alnum_count: int

Number of alphanumeric characters in the TTS side of this segment.

property original_alnum_count: int

Number of alphanumeric characters in the original side of this segment.

class pipecat.utils.context.text_segment_map.TextSegmentMap(tts_text: str, original_text: str, llm_text: str | None = None)[source]

Bases: object

Maps cursor positions between transformed TTS text and original text.

Built once from two texts that may differ in alphanumeric content due to text transforms (e.g. currency expansion). Tracks how many TTS alnum chars have been consumed word-by-word and exposes the corresponding position in the original text.

For unchanged segments, both cursors advance proportionally. For transformed segments, both cursors are held until the entire TTS segment has been consumed, then jump to the end of the corresponding original segment in one step.

Callers advance the map word-by-word via advance_word(), which also strips SSML tag markup from a raw word-timestamp token, tracking tags whose opening tag spans multiple words (e.g. an SSML tag with several attributes, split on whitespace by some TTS providers’ word-timestamp streams).

Example:

# "$42.50" was expanded to "forty two dollars and fifty cents"
smap = TextSegmentMap(
    "Your balance is forty two dollars and fifty cents",
    "Your balance is $42.50",
)
for word in ["Your", "balance", "is"]:
    smap.advance_word(word)   # unchanged segment
for word in ["forty", "two", "dollars", "and", "fifty"]:
    smap.advance_word(word)   # transformed segment, cursors held
smap.advance_word("cents")    # segment completes, cursors jump
assert smap.last_completed_segment.original == "$42.50"
assert not smap.in_transformed_segment
__init__(tts_text: str, original_text: str, llm_text: str | None = None)[source]

Initialize the segment map.

Parameters:
  • tts_text – Post-transform text sent to TTS.

  • original_text – User-facing pre-transform text (no surrounding tags).

  • llm_text – LLM-produced text, which may have surrounding tags like <card>...</card>. Defaults to original_text when not provided. The LLM cursor advances through this text at the same segment boundaries as the user-facing cursor.

strip_word(word: str) str[source]

Return word with any SSML tag markup removed, without consuming it.

Non-mutating peek variant of advance_word()’s stripping step, so callers can decide whether a word belongs here before consuming it.

Parameters:

word – Raw word token to strip.

Returns:

word with tag markup removed.

advance_word(word: str) str[source]

Strip tag markup from word, commit the tag state, and advance.

Combines strip_word() with the internal char-count advance: strips word (tracking any tag left open for the next call), then advances cursors by the resulting content’s alphanumeric character count – zero for a word that is entirely tag markup.

Parameters:

word – Raw TTS word-timestamp token.

Returns:

word with tag markup removed. Callers should use this in place of the raw word for their own alnum accounting.

property user_facing_pos: int

Current byte offset in the original (user-facing) text.

property llm_pos: int

Current byte offset in the LLM text.

property in_transformed_segment: bool

True when the cursor is on a transformed segment that isn’t complete yet.

True once the segment’s alnum budget has partly been consumed (the original condition), or once a call has touched this segment without consuming anything (e.g. a leading zero-alnum fragment such as a still-open tag’s attribute text, which normalizes to "") – as long as that call didn’t simply land here by completing the previous segment, in which case the word that triggered it belongs to that previous segment, not this one.

property last_completed_segment: TextSegment | None

The segment completed by the last advance_word() call, or None.

reset() None[source]

Reset all cursor and consumption state to initial values.