"""Genesys Enhanced text-to-speech — billed in dollars, NOT in tokens. A separate published page, a separate meter, a separate subtotal: https://help.genesys.cloud/articles/genesys-enhanced-tts-pricing/ **last updated 2026-05-22**. TTS never appears in the tokens-model article and is never converted to tokens here. It is carried because Genesys AI spend is understated without it — but it is reported as its own line, and the grand total shows tokens and TTS as two labelled components rather than one blended figure. Three published facts the obvious model misses: * **"The monthly billing unit is one million characters, rounded up."** 200,000 characters bills as $5; 1,000,005 characters bills as $10. * **Free during virtual agents.** "Genesys Enhanced TTS is included free of charge when you use a virtual agent or agentic virtual agent during the interaction" — so deflection cuts this line as well as the token line. * **Standard voices are ending.** End of sale 2026-03-29, end of support 2026-08-05. Selecting Standard for a future-state model is a finding. """ from __future__ import annotations import math from dataclasses import dataclass from .meters import Confidence from .usage import DeflectionMix TTS_SOURCE: str = "https://help.genesys.cloud/articles/genesys-enhanced-tts-pricing/" TTS_SOURCE_DATE: str = "2026-05-22" #: "$20 per one million TTS characters" (advanced: NTTS, neural, wavenet) #: and "$5 per one million TTS characters" (standard). TTS_PRICE_PER_MILLION_CHARS: dict[str, float] = {"advanced": 20.0, "standard": 5.0} #: "The monthly billing unit is one million characters, rounded up." TTS_BILLING_UNIT_CHARS: int = 1_000_000 #: Standard Enhanced TTS lifecycle — published end-of-sale / end-of-support. TTS_STANDARD_END_OF_SALE: str = "2026-03-29" TTS_STANDARD_END_OF_SUPPORT: str = "2026-08-05" #: Published, so 🟢 — unlike the tokens article, this page states the rates. TTS_CONFIDENCE: Confidence = Confidence.CONFIRMED MONTHS_PER_YEAR: int = 12 @dataclass(frozen=True) class TtsUsage: """Synthesised speech volume. Client data, from ``engagement-data``. "Usage includes all TTS characters, including letters, numbers, punctuation, and spaces." """ calls_monthly: float chars_per_call: int = 2_000 tier: str = "advanced" def __post_init__(self) -> None: if self.calls_monthly < 0: raise ValueError("calls_monthly must not be negative") if self.chars_per_call < 0: raise ValueError("chars_per_call must not be negative") if self.tier not in TTS_PRICE_PER_MILLION_CHARS: raise ValueError( f"unknown TTS tier {self.tier!r}; known: " f"{', '.join(sorted(TTS_PRICE_PER_MILLION_CHARS))}" ) @dataclass(frozen=True) class TtsLine: """The TTS bill, reported separately from the token bill.""" tier: str rate_per_million_chars: float total_chars_monthly: float free_share: float free_chars_monthly: float billable_chars_monthly: float billed_millions_monthly: int cost_monthly: float cost_annual: float confidence: Confidence source_url: str source_date: str warnings: tuple[str, ...] = () def tts_free_share(mix: DeflectionMix) -> float: """Share of TTS characters Genesys does not charge for. "included free of charge when you use a virtual agent or agentic virtual agent during the interaction". Reuses the same partition as the token model, so the two lines cannot disagree about how much was deflected. """ return min(1.0, mix.virtual_agent_share + mix.agentic_va_share) def tts_cost( usage: TtsUsage, mix: DeflectionMix | None = None, concession_pct: float = 0.0 ) -> TtsLine: """The monthly and annual TTS bill, with the published per-1M round-up. The round-up lands on the *billable* character total each month, after the virtual-agent free share is removed. """ if not 0.0 <= concession_pct < 1.0: raise ValueError("concession_pct must be in [0, 1)") free_share = tts_free_share(mix) if mix is not None else 0.0 total_chars = usage.calls_monthly * usage.chars_per_call free_chars = total_chars * free_share billable_chars = total_chars - free_chars billed_millions = math.ceil(billable_chars / TTS_BILLING_UNIT_CHARS) rate = TTS_PRICE_PER_MILLION_CHARS[usage.tier] monthly = billed_millions * rate * (1.0 - concession_pct) warnings: list[str] = [] if usage.tier == "standard": warnings.append( "Standard Enhanced TTS reached end of sale on " f"{TTS_STANDARD_END_OF_SALE} and end of support on " f"{TTS_STANDARD_END_OF_SUPPORT} — price a future state on " "advanced voices." ) return TtsLine( tier=usage.tier, rate_per_million_chars=rate, total_chars_monthly=total_chars, free_share=free_share, free_chars_monthly=free_chars, billable_chars_monthly=billable_chars, billed_millions_monthly=billed_millions, cost_monthly=monthly, cost_annual=monthly * MONTHS_PER_YEAR, confidence=TTS_CONFIDENCE, source_url=TTS_SOURCE, source_date=TTS_SOURCE_DATE, warnings=tuple(warnings), )