#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Expand date expressions into spoken form for TTS."""
import re
from datetime import datetime
from num2words import num2words
from pipecat.frames.frames import AggregationType
# ISO dates: 2023-05-10
_ISO_DATE_RE = re.compile(r"\b(\d{4})-(\d{2})-(\d{2})\b")
# US dates: 05/10/2023 or 05-10-2023 (not matched by ISO because ISO has 4-digit year first)
_US_DATE_RE = re.compile(r"\b(\d{1,2})[/\-](\d{1,2})[/\-](\d{4})\b")
_ORDINAL_SUFFIXES = {1: "st", 2: "nd", 3: "rd"}
# Indexed by month - 1. Hardcoded rather than derived from strftime("%B"), which
# renders the month in the process's LC_TIME locale and would produce a
# mixed-language result alongside the English year and ordinal suffix.
_MONTH_NAMES = (
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
)
def _ordinal(n: int) -> str:
suffix = _ORDINAL_SUFFIXES.get(n % 10 if n % 100 not in (11, 12, 13) else 0, "th")
return f"{n}{suffix}"
def _date_to_spoken(dt: datetime) -> str:
year_words = num2words(dt.year, lang="en")
month = _MONTH_NAMES[dt.month - 1]
day = _ordinal(dt.day)
return f"{month} {day}, {year_words}"
def _iso_replace(match: re.Match) -> str:
try:
dt = datetime(int(match.group(1)), int(match.group(2)), int(match.group(3)))
return _date_to_spoken(dt)
except ValueError:
return match.group(0)
def _us_replace(match: re.Match) -> str:
try:
dt = datetime(int(match.group(3)), int(match.group(1)), int(match.group(2)))
return _date_to_spoken(dt)
except ValueError:
return match.group(0)
[docs]
async def normalize_dates(text: str, aggregation_type: str | AggregationType) -> str:
"""Expand date expressions to their spoken form.
Handles ISO format (``YYYY-MM-DD``) and US format (``MM/DD/YYYY`` or ``MM-DD-YYYY``).
Args:
text: Input text possibly containing date expressions.
aggregation_type: Aggregation type of the text frame (unused).
Returns:
Text with date expressions replaced by spoken equivalents.
Example::
result = await normalize_dates("Meeting on 2023-05-10", "*")
# "Meeting on May 10th, two thousand and twenty three"
"""
text = _ISO_DATE_RE.sub(_iso_replace, text)
text = _US_DATE_RE.sub(_us_replace, text)
return text