Source code for pipecat.utils.text.transforms.voice_formatter
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Configurable voice formatting bundle for TTS preprocessing."""
from pipecat.frames.frames import AggregationType
[docs]
class VoiceFormatter:
r"""Configurable bundle that applies a pipeline of voice-formatting transforms.
Each option enables or disables one transform. Transforms are applied in a
deliberate order: structural cleanup first, language expansions second, user
replacements last.
Example::
formatter = VoiceFormatter(
strip_markdown=True,
expand_currency=True,
number_digit_cutoff=2025,
custom_replacements=[(r"\bDr\.", "Doctor")],
)
tts = CartesiaTTSService(
text_transforms=[("*", formatter)],
)
"""
[docs]
def __init__(
self,
*,
strip_markdown: bool = True,
expand_phone_numbers: bool = True,
normalize_acronyms: bool = True,
expand_currency: bool = True,
expand_numbers: bool = False,
number_digit_cutoff: int | None = None,
expand_percentages: bool = True,
expand_units: bool = True,
email_to_speech: bool = True,
normalize_dates: bool = True,
custom_replacements: list[tuple[str, str]] | None = None,
):
"""Initialize the voice formatter.
Args:
strip_markdown: Strip Markdown formatting symbols (bold, italic, headers,
code spans). Enabled by default.
expand_phone_numbers: Space out phone number digits for individual
pronunciation. Enabled by default.
normalize_acronyms: Space out uppercase acronyms (e.g. ``"API"`` →
``"A P I"``). Enabled by default.
expand_currency: Expand currency amounts to spoken form (e.g. ``"$42.50"``
→ ``"forty two dollars and fifty cents"``). Requires ``num2words``.
expand_numbers: Expand numeric digits to spoken words. Disabled by default
since it can affect numbers that are better read as digits. Requires
``num2words``.
number_digit_cutoff: Numbers above this value are read digit-by-digit
instead of as a quantity. Defaults to ``None`` (expand all numbers
as words). Only used when ``expand_numbers=True``.
expand_percentages: Expand percentage expressions (e.g. ``"50%"`` →
``"fifty percent"``). Requires ``num2words``.
expand_units: Expand unit abbreviations (e.g. ``"5km"`` →
``"5 kilometers"``). Enabled by default.
email_to_speech: Transform email addresses to spoken form. Enabled by
default.
normalize_dates: Expand date expressions to spoken form. Requires
``num2words``.
custom_replacements: List of ``(regex_pattern, replacement)`` pairs applied
after all other transforms.
"""
self._transforms = []
if strip_markdown:
from pipecat.utils.text.transforms.strip_markdown import (
strip_markdown as _strip_markdown,
)
self._transforms.append(_strip_markdown)
# email_to_speech must run before expand_phone_numbers (phone regex matches
# digit-only domains) and before normalize_acronyms (all-caps local parts get
# letter-spaced, breaking the email pattern).
if email_to_speech:
from pipecat.utils.text.transforms.email import email_to_speech as _email_to_speech
self._transforms.append(_email_to_speech)
if expand_phone_numbers:
from pipecat.utils.text.transforms.phone import (
expand_phone_numbers as _expand_phone_numbers,
)
self._transforms.append(_expand_phone_numbers)
if normalize_dates:
from pipecat.utils.text.transforms.dates import normalize_dates as _normalize_dates
self._transforms.append(_normalize_dates)
if expand_currency:
from pipecat.utils.text.transforms.currency import expand_currency as _expand_currency
self._transforms.append(_expand_currency)
if expand_percentages:
from pipecat.utils.text.transforms.percentages import (
expand_percentages as _expand_percentages,
)
self._transforms.append(_expand_percentages)
# expand_units must run before normalize_acronyms: uppercase unit abbreviations
# like "MB" or "MPH" would be letter-spaced first and then not recognized.
if expand_units:
from pipecat.utils.text.transforms.units import expand_units as _expand_units
self._transforms.append(_expand_units)
if normalize_acronyms:
from pipecat.utils.text.transforms.acronyms import (
normalize_acronyms as _normalize_acronyms,
)
self._transforms.append(_normalize_acronyms)
if expand_numbers:
from pipecat.utils.text.transforms.numbers import expand_numbers as _expand_numbers
self._transforms.append(_expand_numbers(digit_cutoff=number_digit_cutoff))
if custom_replacements:
from pipecat.utils.text.transforms.replacements import replace_text
self._transforms.append(replace_text(custom_replacements))
async def __call__(self, text: str, aggregation_type: str | AggregationType) -> str:
"""Apply all configured transforms in order.
Args:
text: Input text to transform.
aggregation_type: Aggregation type passed through to each transform.
Returns:
Transformed text ready for TTS synthesis.
"""
for transform in self._transforms:
text = await transform(text, aggregation_type)
return text