148 lines
5.2 KiB
Python
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),
|
|
)
|