openajaj / stt_budget.py
Jindrich3's picture
hynek med
Super-squash branch 'main' using huggingface_hub
b356196
Raw History Blame
4.68 kB
"""Spend limits for the Deepgram speech-to-text proxy.
Deliberately generous — a ceiling on a runaway or a bot, not a quota aimed at
real use. Deepgram is metered (see PRICE_PER_MIN) and the rate-limit
middleware cannot see the WebSocket route: Starlette's BaseHTTPMiddleware skips
non-http scopes, so `_RATE_LIMITS["/api/stt"]` has never applied. Limits are
enforced at the socket or nowhere.
The daily budget is per client. That needs a stable per-client key in the usage
log, but the raw key is a captcha session token or an IP address and neither is
worth storing — so the log holds `client_id()`, a keyed hash under the captcha
secret. It is stable enough to count against and cannot be turned back into an
address by anyone reading the file.
"""
import hashlib
import json
import secrets
import time
MAX_CONCURRENT = 4 # simultaneous microphones per client. Locally,
# and behind the HF router, every browser shares
# one key — so this counts browsers, not people.
MAX_SESSION_S = 8 * 3600 # one microphone may stay open this long: a
# whole party, not a meeting. DAILY_BUDGET_S
# is the real ceiling.
DAILY_BUDGET_S = 10 * 3600 # audio relayed per client per day
DIRECT_SLOT_S = 11 * 60 # a direct socket's slot: its page reports
# usage every 5 min, two missed reports free it
# Deepgram pay-as-you-go streaming, $/min — deepgram.com/pricing, 2026-09-25.
# ponytail: nova-3 is a limited-time promo (regular 0.0077); bump when it ends.
# nova-2 is listed at $0.35/hour. static/transcribe2/index.html mirrors these.
PRICE_PER_MIN = {"nova-3": 0.0048, "nova-3-multi": 0.0058, "nova-2": 0.005833}
DIARIZE_PER_MIN = 0.002 # streaming add-on; the page always asks for it
KEYTERM_PER_MIN = 0.0013 # nova-3 keyterm; nova-2 `keywords` isn't priced
def claim_slot(pages: dict[str, float], sid: str, now: float) -> str | None:
"""Keep `sid`'s slot or open a new one; None when MAX_CONCURRENT are live.
`pages` maps slot id → expiry for one client's sockets straight to
Deepgram EU. The server never sees those sockets, only the token requests
and usage reports of the pages that hold them.
"""
for old in [s for s, until in pages.items() if until < now]:
del pages[old]
if sid not in pages:
if len(pages) >= MAX_CONCURRENT:
return None
sid = secrets.token_urlsafe(9)
pages[sid] = now + DIRECT_SLOT_S
return sid
def client_id(raw_key: str, secret: bytes) -> str:
"""Pseudonymous, stable id for a client. The raw key is never stored."""
return hashlib.blake2b(
raw_key.encode("utf-8", "replace"), key=secret[:64], digest_size=8
).hexdigest()
def cost_per_min(model: str, lang: str, diarize: bool, keyterm: bool) -> float:
"""What Deepgram charges per streamed minute for this request."""
is_multi = model == "nova-3" and lang == "multi"
rate = PRICE_PER_MIN["nova-3-multi" if is_multi else model]
if diarize:
rate += DIARIZE_PER_MIN
if keyterm and model == "nova-3":
rate += KEYTERM_PER_MIN
return rate
def seconds_today(usage_file: str, client: str | None = None,
now: float | None = None) -> float:
"""Sum today's `duration_s`, for one client or for everyone.
Tolerates a missing file and half-written lines: the log is appended to
without locking, so a truncated final line is normal and must not be
allowed to abort the count — under-counting here would silently disable
the daily cap.
"""
today = time.strftime("%Y-%m-%d", time.localtime(now))
total = 0.0
try:
with open(usage_file, encoding="utf-8") as log:
for line in log:
try:
entry = json.loads(line)
except ValueError:
continue
if not isinstance(entry, dict):
continue
if not str(entry.get("ts", "")).startswith(today):
continue
if client is not None and entry.get("client") != client:
continue
duration = entry.get("duration_s") or 0
if isinstance(duration, (int, float)):
total += duration
except FileNotFoundError:
return 0.0
return total
def is_exhausted(usage_file: str, client: str, now: float | None = None) -> bool:
"""True when this client has reached its daily budget."""
return seconds_today(usage_file, client, now) >= DAILY_BUDGET_S