diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d3d97f..15154e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.12.20] — 2026-06-28 + +### Performance +- **Voice list sessionStorage cache** — voices are written to `sessionStorage` after every successful fetch (`ttsvc_vc`). On the next page load the list renders instantly from cache while the fresh fetch runs silently in the background. The Refresh button clears the cache first to guarantee a clean reload. + +--- + ## [1.12.19] — 2026-06-28 ### Fixed diff --git a/VERSION b/VERSION index ea90c61..00f1401 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.19 +1.12.20 diff --git a/static/index.html b/static/index.html index f612459..0803544 100644 --- a/static/index.html +++ b/static/index.html @@ -26,7 +26,7 @@ - + @@ -336,7 +336,7 @@ - + diff --git a/static/js/voice-library.js b/static/js/voice-library.js index 4c43c15..9d3cf5a 100644 --- a/static/js/voice-library.js +++ b/static/js/voice-library.js @@ -42,6 +42,18 @@ let _activePlayVoiceId = null; let _activePlayUrl = null; let _libraryLoadPromise = null; const BENCHMARK_SAMPLE_STORAGE_KEY = 'vcf-benchmark-sample-text'; +const _VL_CACHE_KEY = 'ttsvc_vc'; + +function _vlCacheRead() { + try { return JSON.parse(sessionStorage.getItem(_VL_CACHE_KEY) || 'null'); } catch (_) { return null; } +} +function _vlCacheWrite(voices) { + try { sessionStorage.setItem(_VL_CACHE_KEY, JSON.stringify(voices)); } catch (_) {} +} +function _vlCacheClear() { + try { sessionStorage.removeItem(_VL_CACHE_KEY); } catch (_) {} +} +window._vlCacheClear = _vlCacheClear; const _libraryFilters = {text:'', lang:'', sex:'', type:'', rating:''}; const DEFAULT_BENCHMARK_SAMPLE_TEXT = 'Hello, how are you today? Please read this sample clearly for a fair voice benchmark.'; @@ -613,25 +625,42 @@ async function loadVoiceLibrary() { _libraryLoadPromise = (async () => { setBusyButton('refresh-voices-btn', true); const list = $('voice-list'); - const silent = _voices.length > 0; // already rendered — refresh without blanking the list + + // Serve from sessionStorage cache immediately so the list is never blank on reload + const cached = _voices.length === 0 ? _vlCacheRead() : null; + if (cached && Array.isArray(cached) && cached.length) { + _voices = cached; + window._voices = _voices; + if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices); + renderVoiceList(); + updatePreviewVoiceMatchPanel(); + status(`Loaded ${_voices.length} voices`); + } + + // Always background-fetch fresh data; skip skeleton if something is already shown + const silent = _voices.length > 0; if (list && !silent) list.innerHTML = loadingMarkup('Loading voice library', 'Scanning voices, reference text, metadata, ratings, and benchmark results.', 8); if (!silent) { $('voice-count').textContent = 'Loading voices…'; updateLibraryInsights('loading'); status('Loading voice library…'); } try { const r = await fetch('/api/voices'); if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); } - _voices = await r.json(); - window._voices = _voices; // expose for cross-module access (Script Rehearser etc.) + const fresh = await r.json(); + _vlCacheWrite(fresh); + _voices = fresh; + window._voices = _voices; if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices); renderVoiceList(); updatePreviewVoiceMatchPanel(); if (typeof renderPerfHistory === 'function') renderPerfHistory(); status(`Loaded ${_voices.length} voices`); } catch(e) { - if (list) list.innerHTML = '
Failed to load voices: ' + (e.message||String(e)) + '
'; - $('voice-count').textContent = 'Load failed'; - updateLibraryInsights('error'); + if (!silent) { + if (list) list.innerHTML = '
Failed to load voices: ' + (e.message||String(e)) + '
'; + $('voice-count').textContent = 'Load failed'; + updateLibraryInsights('error'); + } status('Voice library load failed: ' + e.message); - throw e; + if (!silent) throw e; } finally { setBusyButton('refresh-voices-btn', false); _libraryLoadPromise = null; @@ -640,7 +669,7 @@ async function loadVoiceLibrary() { return _libraryLoadPromise; } -$('refresh-voices-btn').addEventListener('click', loadVoiceLibrary); +$('refresh-voices-btn').addEventListener('click', () => { _vlCacheClear(); loadVoiceLibrary(); }); $('sync-voice-folders-btn').addEventListener('click', async () => { $('sync-voice-folders-btn').disabled = true; status('Syncing active_voices and hidden_voices…');