feat: deduplicate voice design requests
This commit is contained in:
parent
116e234c20
commit
2b99dc8ccc
@ -51,6 +51,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
prompt/source content and sensitive result fields are excluded from the audit record.
|
||||
- **Synthesized Audio Cache Eviction.** Persisted line-audio clips now use configurable global file-count and byte
|
||||
limits with oldest-first eviction, and oversized individual clips are rejected.
|
||||
- **Voice Design Idempotency.** Repeated voice-design submissions with the same `Idempotency-Key` now reuse the
|
||||
original temporary result; reusing a key for different request data is rejected.
|
||||
- **Security Regression Coverage.** Added a Python test gate for character metadata validation, private-download URL
|
||||
rejection and oversized JSON request rejection.
|
||||
- **Navigation Reliability.** Rehearser phase requests now survive the feature-module initialization boundary.
|
||||
|
||||
4
TODO.md
4
TODO.md
@ -51,8 +51,8 @@ the first block before investing in architecture or polish.
|
||||
- [ ] **M — Make profile portraits and character sheets use one canonical identity.**
|
||||
Shared structured prompts now version consistent pairs as v3 and fingerprint canonical appearance fields; add
|
||||
cross-record validation so mismatched profile/reference files are detected even when both are current-version.
|
||||
- [ ] **M — Add bounded duplicate-generation protection.** Consistent portrait/reference jobs now deduplicate
|
||||
repeated requests with `Idempotency-Key`; extend the same bounded contract to voice and casting jobs.
|
||||
- [ ] **M — Add bounded duplicate-generation protection.** Consistent portrait/reference and voice-design jobs now
|
||||
deduplicate repeated requests with a bounded, expiring `Idempotency-Key` contract; casting jobs remain.
|
||||
- [ ] **M — Improve large-table performance.** Character productions and the flat voice library now paginate at 50
|
||||
visible rows; updating only affected rows and disabling preloading on low-memory devices remain.
|
||||
- [ ] **M — Add explicit cache invalidation and eviction.** Fish.audio search responses and persisted synthesized
|
||||
|
||||
@ -5,6 +5,7 @@ import asyncio
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
@ -55,6 +56,8 @@ logger = logging.getLogger("uvicorn.error")
|
||||
router = APIRouter()
|
||||
_SEED_FINDER_VOICE_LOCK = threading.Lock()
|
||||
_TTS_GENERATION_SLOTS = asyncio.Semaphore(2)
|
||||
_VOICE_DESIGN_IDEMPOTENCY: dict[str, tuple[str, dict, float]] = {}
|
||||
_VOICE_DESIGN_IDEMPOTENCY_LOCK = threading.Lock()
|
||||
|
||||
|
||||
async def _bounded_tts_call(fn, *args):
|
||||
@ -777,6 +780,21 @@ async def tts_style_variation(request: Request):
|
||||
@router.post("/api/voice-design")
|
||||
async def voice_design(request: Request):
|
||||
data = await request.json()
|
||||
idempotency_key = str(request.headers.get("idempotency-key") or "").strip()
|
||||
if len(idempotency_key) > 200:
|
||||
raise HTTPException(400, "Idempotency-Key is too long")
|
||||
request_fingerprint = hashlib.sha256(json.dumps(data, sort_keys=True, default=str).encode()).hexdigest()
|
||||
if idempotency_key:
|
||||
now = time.time()
|
||||
with _VOICE_DESIGN_IDEMPOTENCY_LOCK:
|
||||
for old_key, (_, _, created) in list(_VOICE_DESIGN_IDEMPOTENCY.items()):
|
||||
if now - created > 1800:
|
||||
_VOICE_DESIGN_IDEMPOTENCY.pop(old_key, None)
|
||||
previous = _VOICE_DESIGN_IDEMPOTENCY.get(idempotency_key)
|
||||
if previous:
|
||||
if previous[0] != request_fingerprint:
|
||||
raise HTTPException(409, "Idempotency-Key was already used for a different request")
|
||||
return {**previous[1], "deduplicated": True}
|
||||
instruct: str = data.get("instruct", "").strip()
|
||||
sample_text: str = data.get("sample_text", "Hello! This is a voice design sample.").strip()
|
||||
language: str = data.get("language", "Auto")
|
||||
@ -807,7 +825,14 @@ async def voice_design(request: Request):
|
||||
tmp.write_bytes(audio)
|
||||
fid = uuid.uuid4().hex
|
||||
_registry_put(fid, tmp)
|
||||
return {"id": fid, "duration": _duration(tmp)}
|
||||
result = {"id": fid, "duration": _duration(tmp)}
|
||||
if idempotency_key:
|
||||
with _VOICE_DESIGN_IDEMPOTENCY_LOCK:
|
||||
_VOICE_DESIGN_IDEMPOTENCY[idempotency_key] = (request_fingerprint, result, time.time())
|
||||
if len(_VOICE_DESIGN_IDEMPOTENCY) > 256:
|
||||
oldest = min(_VOICE_DESIGN_IDEMPOTENCY, key=lambda k: _VOICE_DESIGN_IDEMPOTENCY[k][2])
|
||||
_VOICE_DESIGN_IDEMPOTENCY.pop(oldest, None)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/api/tts-route-test")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user