From ad155f6c5cc7edabf725ef6f4d53cce18816fdd0 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Sun, 28 Jun 2026 02:00:48 +0200 Subject: [PATCH] =?UTF-8?q?perf:=20voice=20list=20sessionStorage=20cache?= =?UTF-8?q?=20=E2=80=94=20instant=20render=20on=20reload=20(v1.12.20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a successful /api/voices fetch, results are written to sessionStorage (key ttsvc_vc). On the next page load loadVoiceLibrary() reads the cache and renders voices immediately before the network request completes, eliminating the blank-list / skeleton flash entirely. The background fetch always runs and overwrites the cache with fresh data. Error handling is graceful: if the network fails but cache was served, the error is suppressed (stale data stays visible). The Refresh button clears the cache first to force a full reload cycle. Silent-refresh logic (from v1.12.19) prevents the second loadVoiceLibrary() call (engines.js batch E) from blanking already-rendered voices. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 7 ++++++ VERSION | 2 +- static/index.html | 4 ++-- static/js/voice-library.js | 45 +++++++++++++++++++++++++++++++------- 4 files changed, 47 insertions(+), 11 deletions(-) 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…');