Skip to content

Commit 5119ff4

Browse files
committed
feat: JSON-body cache opt-out (v2.0.3)
Adds a first-class request field `cache: bool` that lives alongside the existing HTTP `Cache-Control: no-cache` header support. Both turn off read + write of the audio cache for the single request. JSON: {"input": "...", "voice": "alloy", "cache": false} Multipart: curl ... -F cache=0 -F input=... Header: curl ... -H "Cache-Control: no-cache" ... The three mechanisms are equivalent and interchangeable. Body field is the OpenAI-style extension; the header is there for curl/standard HTTP clients. Keeps symmetry with uttera-tts-vllm v0.1.4. SERVER_VERSION bumped to 2.0.3.
1 parent 8cfe1ae commit 5119ff4

2 files changed

Lines changed: 38 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.0.3] - 2026-04-17
9+
10+
### Added
11+
- JSON-body cache opt-out alongside the HTTP-header one. Clients can
12+
now send `{"cache": false}` in the request body (or the string
13+
`"0"`, `"false"`, `"no"`, `"off"` in multipart/urlencoded form) and
14+
the server skips the audio cache for that single request. Keeps
15+
symmetry with `uttera-tts-vllm` v0.1.4 so the same client code works
16+
against either backend.
17+
818
## [2.0.2] - 2026-04-17
919

1020
### Added

main_tts.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@
1919
# VoxCPM2, …), personality tuning, and GIL-bypass concurrency.
2020
#
2121
# CHANGELOG:
22+
# - 2.0.3 (2026-04-17): JSON-body cache opt-out. Clients can now send
23+
# {"cache": false} (JSON) or cache=0/false/no/off (form) to skip the
24+
# audio cache for that single request. Symmetric with the existing
25+
# Cache-Control header support and with uttera-tts-vllm v0.1.4.
26+
# - 2.0.2 (2026-04-17): Per-request cache bypass via Cache-Control
27+
# HTTP header + response header X-Cache: HIT/MISS/BYPASS/DISABLED.
28+
# New COLD_VRAM_HEADROOM_GB (default 2.0) reserved on top of the
29+
# projected cold-pool consumption in _has_vram_for_cold_lane to
30+
# prevent cascading OOMs with big backends (VoxCPM2 at ~8 GB per
31+
# cold worker on a 32 GB card).
32+
# - 2.0.1 (2026-04-17): CACHE_TTL_MINUTES=0 now truly disables the
33+
# cache (previously served every hit regardless of age and kept
34+
# populating on-disk entries — silent bug surfaced by benchmark
35+
# runs against small fixed corpora).
2236
# - 2.0.0 (2026-04-16): First Uttera-branded release. BREAKING:
2337
# * Plugin-based backend architecture. Inference now goes through
2438
# backends.TTSBackend (ABC) + factory keyed on TTS_BACKEND env var.
@@ -285,7 +299,7 @@ def find_venv_path(rel_path):
285299
REDIS_KEY = f"tts:nodes:{REDIS_NODE_ID}"
286300
REDIS_TTL = max(2, int(COLD_POOL_MANAGER_INTERVAL * 3 + 1)) # seconds
287301

288-
SERVER_VERSION = "2.0.2"
302+
SERVER_VERSION = "2.0.3"
289303

290304
# -------------------------------
291305
# 2. Voice Mapping — loaded from VOICE_ASSET_DIR/voices.json
@@ -330,6 +344,11 @@ class SpeechRequest(BaseModel):
330344
# When omitted (None), VoxCPM maps temperature → cfg_value automatically.
331345
cfg_value: Optional[float] = None
332346
inference_timesteps: Optional[int] = None
347+
# Opt out of the server-side audio cache for this specific request. When
348+
# False the server neither reads nor writes the MD5-keyed audio cache;
349+
# the response carries `X-Cache: BYPASS`. Omit (None) to fall back to
350+
# the server default (driven by `CACHE_TTL_MINUTES`).
351+
cache: Optional[bool] = None
333352

334353
# -------------------------------
335354
# 4. Hot Model Loading (through backend plugin)
@@ -992,7 +1011,7 @@ async def health_check():
9921011
# -------------------------------
9931012
# 13. Endpoint: POST /v1/audio/speech
9941013
# -------------------------------
995-
def _cache_bypass_requested(request: Request) -> bool:
1014+
def _cache_header_bypass(request: Request) -> bool:
9961015
"""Honour the standard HTTP `Cache-Control: no-cache` header as an opt-out
9971016
for this specific request, without requiring the operator to set
9981017
`CACHE_TTL_MINUTES=0` globally. Bench harnesses and clients that want
@@ -1005,13 +1024,17 @@ def _cache_bypass_requested(request: Request) -> bool:
10051024
@app.post("/v1/audio/speech")
10061025
async def create_speech(request: Request, background_tasks: BackgroundTasks):
10071026
content_type = request.headers.get("Content-Type", "")
1008-
bypass_cache = _cache_bypass_requested(request)
1027+
header_bypass = _cache_header_bypass(request)
10091028
if "application/json" in content_type:
10101029
data = await request.json()
10111030
req = SpeechRequest(**data)
10121031
custom_wav_path = None
10131032
else:
10141033
form_data = await request.form()
1034+
_raw_cache = form_data.get("cache")
1035+
_cache_field: Optional[bool] = None
1036+
if _raw_cache is not None:
1037+
_cache_field = str(_raw_cache).strip().lower() not in ("0", "false", "no", "off")
10151038
req = SpeechRequest(
10161039
input=form_data.get("input"),
10171040
voice=form_data.get("voice", DEFAULT_VOICE),
@@ -1023,6 +1046,7 @@ async def create_speech(request: Request, background_tasks: BackgroundTasks):
10231046
repetition_penalty=float(form_data.get("repetition_penalty", os.environ.get("DEFAULT_REPETITION_PENALTY", 5.0))),
10241047
top_k=int(form_data.get("top_k", os.environ.get("DEFAULT_TOP_K", 50))),
10251048
top_p=float(form_data.get("top_p", os.environ.get("DEFAULT_TOP_P", 0.85))),
1049+
cache=_cache_field,
10261050
)
10271051
custom_file = form_data.get("custom_voice_file")
10281052
if custom_file and isinstance(custom_file, UploadFile):
@@ -1066,6 +1090,7 @@ async def create_speech(request: Request, background_tasks: BackgroundTasks):
10661090
).hexdigest()
10671091
final_output_path = os.path.join(AUDIO_CACHE_DIR, f"{cache_key}.{req.response_format}")
10681092

1093+
bypass_cache = header_bypass or (req.cache is False)
10691094
cache_effectively_on = CACHE_TTL_MINUTES > 0 and not bypass_cache
10701095
cache_lock = _cache_locks.setdefault(cache_key, asyncio.Lock())
10711096
async with cache_lock:

0 commit comments

Comments
 (0)