perf: bound persisted line audio cache
This commit is contained in:
parent
d21a6f28e8
commit
116e234c20
@ -49,6 +49,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
filenames before deriving extensions; voice ZIP imports also reject absolute, traversal and malformed archive paths.
|
||||
- **Generation Audit History.** Prompt and image generation now record bounded metadata in the protected history API;
|
||||
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.
|
||||
- **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.
|
||||
|
||||
7
TODO.md
7
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.** 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 now have bounded eviction;
|
||||
add equivalent LRU cleanup for audio/image entries and object URLs.
|
||||
- [ ] **M — Add explicit cache invalidation and eviction.** Fish.audio search responses and persisted synthesized
|
||||
line-audio clips now have bounded eviction; image entries and browser object URLs still need equivalent cleanup.
|
||||
- [ ] **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,
|
||||
@ -109,7 +109,8 @@ the first block before investing in architecture or polish.
|
||||
- [ ] Cache `/api/voices` with explicit invalidation instead of repeated full scans.
|
||||
- [x] Lazy-load profile images and concept art.
|
||||
- [ ] Update only affected table rows instead of rebuilding entire tables.
|
||||
- [ ] Add an LRU cache for audio and images and remove old entries automatically.
|
||||
- [ ] Add an LRU cache for audio and images and remove old entries automatically. Persisted line-audio is now bounded;
|
||||
image-cache eviction remains.
|
||||
- [x] Revoke discarded object URLs with `URL.revokeObjectURL(objectUrl)`; completed synthesized voice previews now release their Blob URLs when playback ends.
|
||||
- [x] Add low-memory mode using `navigator.deviceMemory` where available; shared worker pools cap at two tasks on devices reporting approximately 4 GB RAM or less.
|
||||
- [ ] Disable image preloading and reduce table/cache sizes on devices with approximately 4 GB RAM.
|
||||
|
||||
@ -1260,6 +1260,34 @@ async def encode_mp3(request: Request):
|
||||
# audio.
|
||||
_LINE_AUDIO_DIR = CONFIG_DIR / "line_audio_cache"
|
||||
_LINE_AUDIO_KEY_RE = re.compile(r"^[a-f0-9]{16,64}$")
|
||||
_LINE_AUDIO_MAX_FILES = max(1, int(os.environ.get("LINE_AUDIO_CACHE_MAX_FILES", "2000")))
|
||||
_LINE_AUDIO_MAX_BYTES = max(1, int(os.environ.get("LINE_AUDIO_CACHE_MAX_MB", "1024"))) * 1024 * 1024
|
||||
|
||||
|
||||
def _evict_line_audio_cache() -> int:
|
||||
"""Evict least-recently-written line clips until the global cache is bounded."""
|
||||
if not _LINE_AUDIO_DIR.is_dir():
|
||||
return 0
|
||||
files = []
|
||||
total = 0
|
||||
for path in _LINE_AUDIO_DIR.glob("*/[a-f0-9]*.wav"):
|
||||
try:
|
||||
stat = path.stat()
|
||||
except OSError:
|
||||
continue
|
||||
files.append((stat.st_mtime, path, stat.st_size))
|
||||
total += stat.st_size
|
||||
removed = 0
|
||||
for _, path, size in sorted(files):
|
||||
if len(files) - removed <= _LINE_AUDIO_MAX_FILES and total <= _LINE_AUDIO_MAX_BYTES:
|
||||
break
|
||||
try:
|
||||
path.unlink()
|
||||
total -= size
|
||||
removed += 1
|
||||
except OSError:
|
||||
pass
|
||||
return removed
|
||||
|
||||
|
||||
def _line_audio_book_dir(book: str) -> Path:
|
||||
@ -1374,8 +1402,11 @@ async def put_line_audio(book: str, key: str, request: Request):
|
||||
wav_bytes = await request.body()
|
||||
if not wav_bytes:
|
||||
raise HTTPException(400, "Empty request body")
|
||||
if len(wav_bytes) > _LINE_AUDIO_MAX_BYTES:
|
||||
raise HTTPException(413, "Cached audio clip is too large")
|
||||
path = _line_audio_book_dir(book) / f"{key}.wav"
|
||||
path.write_bytes(wav_bytes)
|
||||
_evict_line_audio_cache()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
|
||||
@ -119,6 +119,27 @@ class SecurityValidationTests(unittest.TestCase):
|
||||
self.assertEqual(rows[0]["prompt"], "")
|
||||
self.assertNotIn("must-not-be-added", rows[0]["result"])
|
||||
|
||||
def test_line_audio_cache_eviction_is_bounded(self):
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
import routes.tts as tts
|
||||
with TemporaryDirectory() as directory:
|
||||
old_dir, old_count, old_bytes = tts._LINE_AUDIO_DIR, tts._LINE_AUDIO_MAX_FILES, tts._LINE_AUDIO_MAX_BYTES
|
||||
try:
|
||||
tts._LINE_AUDIO_DIR = Path(directory)
|
||||
tts._LINE_AUDIO_MAX_FILES = 2
|
||||
tts._LINE_AUDIO_MAX_BYTES = 10
|
||||
book = Path(directory) / "book"
|
||||
book.mkdir()
|
||||
for index in range(4):
|
||||
(book / f"{index:032x}.wav").write_bytes(b"1234")
|
||||
tts._evict_line_audio_cache()
|
||||
files = list(book.glob("*.wav"))
|
||||
self.assertLessEqual(len(files), 2)
|
||||
self.assertLessEqual(sum(path.stat().st_size for path in files), 10)
|
||||
finally:
|
||||
tts._LINE_AUDIO_DIR, tts._LINE_AUDIO_MAX_FILES, tts._LINE_AUDIO_MAX_BYTES = old_dir, old_count, old_bytes
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user