perf: paginate large voice libraries

This commit is contained in:
mARTin-B78 2026-09-04 08:05:09 +02:00
parent 8235b36afd
commit e52d462dac
3 changed files with 33 additions and 3 deletions

View File

@ -43,6 +43,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
changing those fields causes the existing pair to be flagged stale.
- **Character Table Pagination.** Productions now render 50 character cards/rows at a time with an explicit Show more
control, keeping sorting and record lookups intact while reducing DOM and image work for large casts.
- **Voice Table Pagination.** The voice library now renders up to 50 sorted rows initially and adds a Show more
control for larger result sets, while retaining the full dataset for filtering and sorting.
- **Upload Filename Boundaries.** Audio, STT, route-sound and picture uploads now reject path-like or control-character
filenames before deriving extensions; voice ZIP imports also reject absolute, traversal and malformed archive paths.
- **Security Regression Coverage.** Added a Python test gate for character metadata validation, private-download URL

View File

@ -53,8 +53,8 @@ the first block before investing in architecture or polish.
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 — Improve large-table performance.** Character productions now paginate at 50 visible rows; paginate voice
tables, update only affected rows, and disable preloading on low-memory devices.
- [ ] **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 constrained-device validation.** Test with Chrome CPU/memory throttling,

View File

@ -1816,6 +1816,22 @@ function renderVoiceList() {
return 0;
});
// Keep the complete filtered/sorted list in memory, but avoid creating a
// large DOM (and hundreds of audio/image nodes) for big libraries.
const pageSignature = JSON.stringify([
cat, showDisabled, window._voiceGroupFilter || '', window._voiceTagFilter || '',
_libraryIssueFilter || '', _sortField, _sortDir, !!window._voiceTableView, _libraryFilters,
]);
if (window._voicePageSignature !== pageSignature) {
window._voicePageSignature = pageSignature;
window._voicePage = 1;
}
const totalFiltered = filtered.length;
const pageSize = 50;
const page = Math.max(1, Number(window._voicePage) || 1);
const pageEnd = Math.min(totalFiltered, page * pageSize);
const pageItems = filtered.slice(0, pageEnd);
updateLibraryInsights();
if (_libraryIssueFilter) {
const note = document.createElement('div');
@ -1955,8 +1971,20 @@ function renderVoiceList() {
if (window._voiceGroupByTag) renderVoicesByTag(list, filtered);
else {
const frag = document.createDocumentFragment();
filtered.forEach(v => frag.appendChild(makeVoiceRow(v)));
pageItems.forEach(v => frag.appendChild(makeVoiceRow(v)));
list.appendChild(frag);
if (pageEnd < totalFiltered) {
const more = document.createElement('button');
more.type = 'button';
more.className = 'btn-secondary vl-load-more';
more.style.margin = '12px 0';
more.textContent = `Show more voices (${pageEnd} / ${totalFiltered})`;
more.addEventListener('click', () => {
window._voicePage = page + 1;
renderVoiceList();
});
list.appendChild(more);
}
}
if (_pendingSelectId) {
const wrap = list.querySelector(`[data-id="${CSS.escape(_pendingSelectId)}"]`);