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):
285299REDIS_KEY = f"tts:nodes:{ REDIS_NODE_ID } "
286300REDIS_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" )
10061025async 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