perf: bound fish audio response cache
This commit is contained in:
parent
885fa9b065
commit
4a0101f4a4
@ -27,6 +27,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
existing job for repeated submissions; completed job mappings are evicted with the job cache.
|
||||
- **Image Concurrency Bound.** Consistent image generation is limited to two active GPU jobs; additional requests
|
||||
remain observable as queued jobs and fail clearly if no slot becomes available within 15 minutes.
|
||||
- **Fish.audio Cache Eviction.** Offline search responses are now evicted by configurable file-count and byte-size
|
||||
limits, preventing an unbounded cache from consuming the configuration volume.
|
||||
- **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
@ -55,8 +55,8 @@ the first block before investing in architecture or polish.
|
||||
repeated requests with `Idempotency-Key`; extend the same bounded contract to voice and casting jobs.
|
||||
- [ ] **M — Improve large-table performance.** Virtualize/paginate character and voice
|
||||
tables, update only affected rows, and disable preloading on low-memory devices.
|
||||
- [ ] **M — Add explicit cache invalidation and eviction.** Cache `/api/voices` safely
|
||||
and add LRU cleanup for audio/image entries and object URLs.
|
||||
- [ ] **M — Add explicit cache invalidation and eviction.** Fish.audio search responses now have bounded eviction;
|
||||
add equivalent LRU cleanup for audio/image entries and object URLs.
|
||||
- [ ] **M — Add constrained-device validation.** Test with Chrome CPU/memory throttling,
|
||||
a 4 GB VM/container, and measure browser, Docker and temporary-disk usage.
|
||||
- [ ] **L — Add background jobs and bounded queues.** Give voice design, transcription,
|
||||
|
||||
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
@ -29,6 +30,26 @@ from core.voice_index import upsert_voice_in_index
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_FISH_CACHE_MAX_FILES = int(os.environ.get("FISHAUDIO_CACHE_MAX_FILES", "200"))
|
||||
_FISH_CACHE_MAX_BYTES = int(os.environ.get("FISHAUDIO_CACHE_MAX_MB", "256")) * 1024 * 1024
|
||||
|
||||
|
||||
def _evict_fish_cache(cache_dir: Path) -> None:
|
||||
"""Keep the offline search cache bounded by count and total bytes."""
|
||||
try:
|
||||
files = [p for p in cache_dir.glob("*.json") if p.is_file()]
|
||||
total = sum(p.stat().st_size for p in files)
|
||||
files.sort(key=lambda p: p.stat().st_mtime)
|
||||
while files and (len(files) > _FISH_CACHE_MAX_FILES or total > _FISH_CACHE_MAX_BYTES):
|
||||
old = files.pop(0)
|
||||
size = old.stat().st_size
|
||||
old.unlink()
|
||||
total -= size
|
||||
except OSError:
|
||||
# Cache maintenance must never turn a successful provider response
|
||||
# into an API failure.
|
||||
return
|
||||
|
||||
|
||||
async def _prepare_imported_voice(audio: Path, settings: dict) -> None:
|
||||
"""Make a downloaded reference usable for Qwen clone playback.
|
||||
@ -798,6 +819,7 @@ async def fishaudio_voices(request: Request):
|
||||
try:
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
cache_file.write_text(json.dumps(result), encoding="utf-8")
|
||||
_evict_fish_cache(cache_dir)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
@ -78,6 +78,23 @@ class SecurityValidationTests(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(_load_settings().get("llm_api_key"), "keep-this-secret")
|
||||
|
||||
def test_fish_cache_eviction_is_bounded(self):
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
import routes.sources as sources
|
||||
with TemporaryDirectory() as directory:
|
||||
cache = Path(directory)
|
||||
old_count, old_bytes = sources._FISH_CACHE_MAX_FILES, sources._FISH_CACHE_MAX_BYTES
|
||||
try:
|
||||
sources._FISH_CACHE_MAX_FILES = 2
|
||||
sources._FISH_CACHE_MAX_BYTES = 1024
|
||||
for index in range(4):
|
||||
(cache / f"{index}.json").write_text("{}")
|
||||
sources._evict_fish_cache(cache)
|
||||
self.assertLessEqual(len(list(cache.glob("*.json"))), 2)
|
||||
finally:
|
||||
sources._FISH_CACHE_MAX_FILES, sources._FISH_CACHE_MAX_BYTES = old_count, old_bytes
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user