Files
palladium/calculators/Genesys_Token_Calculator/genesyscalc/tts.py

148 lines
5.2 KiB
Python

"""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),
)