Module agents.tokenize.verbalize

Functions

def expand_cardinals(text: str, *, language: str) ‑> str
Expand source code
def expand_cardinals(text: str, *, language: str) -> str:
    """Verbalize standalone cardinal numbers (≥100 or comma-grouped).

    Skips 4-digit years (1700–2099) and numbers inside ``<spell>…</spell>``.
    Decimals are read as "integer point digit-digit".
    """
    lang = (language or "en").lower()
    verb = _pick_verbalizer(lang)

    def _sub(m: re.Match[str]) -> str:
        int_part, frac, suffix = m.group(1), m.group(2), m.group(3)
        digits_only = int_part.replace(",", "")
        if (
            not frac
            and not suffix
            and "," not in int_part
            and len(digits_only) == 4
            and 1700 <= int(digits_only) <= 2099
        ):
            return m.group(0)
        if frac and all(d == "0" for d in frac):
            frac = None
        if frac and not suffix:
            return verbalize_decimal(int_part, frac, lang)
        value, _ = _expand_scale(int_part, frac, suffix)
        return verb(value)

    return _apply_outside_spell(text, lambda t: _CARDINAL_RE.sub(_sub, t))

Verbalize standalone cardinal numbers (≥100 or comma-grouped).

Skips 4-digit years (1700–2099) and numbers inside <spell>…</spell>. Decimals are read as "integer point digit-digit".

def expand_currency(text: str, *, language: str, hint: str | None) ‑> str
Expand source code
def expand_currency(text: str, *, language: str, hint: str | None) -> str:
    """Replace ``$500,000`` / ``₹10,00,000`` with spoken form.

    All-zero fractional parts (``₹50,00,000.00``, ``$1,000.00``) are dropped:
    voice agents should not read "fifty lakh point zero zero rupees" when the
    intent is just "fifty lakh rupees". LLMs (especially Gemini) routinely
    append ``.00`` to round rupee/dollar amounts — treat it as display
    formatting, not spoken content.
    """
    lang = (language or "en").lower()
    verb = _pick_verbalizer(lang)

    def _sub(m: re.Match[str]) -> str:
        symbol, int_part, frac, suffix = m.group(1), m.group(2), m.group(3), m.group(4)
        currency_word = _resolve_currency_word(symbol, hint, lang)
        if frac and all(d == "0" for d in frac):
            frac = None
        if frac and not suffix:
            number_words = verbalize_decimal(int_part, frac, lang)
        else:
            value, _ = _expand_scale(int_part, frac, suffix)
            number_words = verb(value)
        if currency_word:
            return f"{number_words} {currency_word}"
        return number_words

    return _apply_outside_spell(text, lambda t: _CURRENCY_AMOUNT_RE.sub(_sub, t))

Replace $500,000 / ₹10,00,000 with spoken form.

All-zero fractional parts (₹50,00,000.00, $1,000.00) are dropped: voice agents should not read "fifty lakh point zero zero rupees" when the intent is just "fifty lakh rupees". LLMs (especially Gemini) routinely append .00 to round rupee/dollar amounts — treat it as display formatting, not spoken content.

def verbalize_decimal(int_part: str, frac_part: str, lang: str) ‑> str
Expand source code
def verbalize_decimal(int_part: str, frac_part: str, lang: str) -> str:
    """Verbalize a decimal like ``3.5`` → ``three point five``.

    The fractional part is read digit-by-digit (so ``3.14`` → ``three point
    one four``) which is the natural spoken form for non-money decimals.
    """
    int_val = int(int_part.replace(",", "")) if int_part else 0
    verb = _pick_verbalizer(lang)
    head = verb(int_val)
    if frac_part:
        return f"{head} {_point_word(lang)} {_digit_words(frac_part, lang)}"
    return head

Verbalize a decimal like 3.5 → three point five.

The fractional part is read digit-by-digit (so 3.14 → three point one four) which is the natural spoken form for non-money decimals.

def verbalize_en(n: int) ‑> str
Expand source code
def verbalize_en(n: int) -> str:
    """Return English cardinal words for an integer."""
    if n == 0:
        return "zero"
    sign = ""
    if n < 0:
        sign = "minus "
        n = -n
    groups: list[int] = []
    while n > 0:
        n, r = divmod(n, 1000)
        groups.append(r)
    parts: list[str] = []
    for i in range(len(groups) - 1, -1, -1):
        g = groups[i]
        if g == 0:
            continue
        words = _en_under_1000(g)
        if i < len(_EN_SCALES) and _EN_SCALES[i]:
            parts.append(f"{words} {_EN_SCALES[i]}")
        else:
            parts.append(words)
    return sign + " ".join(parts)

Return English cardinal words for an integer.

def verbalize_gu(n: int) ‑> str
Expand source code
def verbalize_gu(n: int) -> str:
    """Gujarati cardinal words via Indian grouping (હજાર / લાખ / કરોડ)."""
    return _indic_verbalize(
        n,
        zero_to_99=_GU_0_99,
        hundred=_GU_HUNDRED, thousand=_GU_THOUSAND,
        lakh=_GU_LAKH, crore=_GU_CRORE, arab=_GU_ARAB,
        neg_prefix="ઓછું",
    )

Gujarati cardinal words via Indian grouping (હજાર / લાખ / કરોડ).

def verbalize_hi(n: int) ‑> str
Expand source code
def verbalize_hi(n: int) -> str:
    """Hindi cardinal words via Indian grouping (हज़ार / लाख / करोड़)."""
    return _indic_verbalize(
        n,
        zero_to_99=_HI_0_99,
        hundred=_HI_HUNDRED, thousand=_HI_THOUSAND,
        lakh=_HI_LAKH, crore=_HI_CRORE, arab=_HI_ARAB,
        neg_prefix="ऋण",
    )

Hindi cardinal words via Indian grouping (हज़ार / लाख / करोड़).

def verbalize_mr(n: int) ‑> str
Expand source code
def verbalize_mr(n: int) -> str:
    """Marathi cardinal words via Indian grouping (हजार / लाख / कोटी)."""
    return _indic_verbalize(
        n,
        zero_to_99=_MR_0_99,
        hundred=_MR_HUNDRED, thousand=_MR_THOUSAND,
        lakh=_MR_LAKH, crore=_MR_CRORE, arab=_MR_ARAB,
        neg_prefix="उणे",
    )

Marathi cardinal words via Indian grouping (हजार / लाख / कोटी).