Introduces the new Studio section (Source -> Characters -> Voices -> Perform & Export) that reuses the existing Read Aloud/Library/Script Rehearsal code via DOM reparenting instead of duplicating it, and rolls up a long tail of bugs found while producing a real audiobook through it: umlaut-eating name sanitizers, a voice picker that mispositioned itself and capped results at 60, PDF pagination silently breaking on trimmed \f markers, a race letting stale audio keep playing after a new line was clicked, an alias-overlap bug that could silently redirect a voice/image save onto the wrong character, voice design failing outright during brief TTS backend restarts instead of retrying, sparse cast entries defaulting to English/wrong gender, and a reassigned voice never reaching an already-open Stage session or invalidating its cached audio. Also adds a persistent per-line audio cache, audiobook export browsing/download, and an inline voice-design prompt editor. Full details in CHANGELOG.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
45 lines
1.8 KiB
JavaScript
45 lines
1.8 KiB
JavaScript
/* TTS Voice Creator — network-first service worker.
|
|
Always tries the network first (so live edits/new versions show immediately) and
|
|
only falls back to cache when offline. Never touches the API or cross-origin
|
|
requests, so it can't serve stale data or break audio/streaming. */
|
|
const CACHE = 'tts-vc-v1';
|
|
|
|
self.addEventListener('install', () => self.skipWaiting());
|
|
|
|
self.addEventListener('activate', (e) => {
|
|
e.waitUntil((async () => {
|
|
const keys = await caches.keys();
|
|
await Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)));
|
|
await self.clients.claim();
|
|
})());
|
|
});
|
|
|
|
self.addEventListener('fetch', (e) => {
|
|
const req = e.request;
|
|
if (req.method !== 'GET') return;
|
|
const url = new URL(req.url);
|
|
if (url.origin !== self.location.origin) return; // ignore CDN / cross-origin
|
|
if (url.pathname.startsWith('/api/')) return; // never cache the API
|
|
e.respondWith((async () => {
|
|
try {
|
|
// cache:'reload' forces this inner fetch to bypass the HTTP cache and go
|
|
// to the network every time. Without it, a browser's Ctrl+Shift+R hard
|
|
// reload does NOT propagate its cache-bypass into fetch() calls made from
|
|
// inside a service worker's fetch handler — so this "network-first" SW
|
|
// could still hand back a stale cached response even right after a hard
|
|
// reload, which is exactly what made JS edits look like they never landed.
|
|
const net = await fetch(req, { cache: 'reload' });
|
|
// Cache only successful static shell assets for offline fallback
|
|
if (net && net.ok && (url.pathname === '/' || url.pathname.startsWith('/static/'))) {
|
|
const c = await caches.open(CACHE);
|
|
c.put(req, net.clone());
|
|
}
|
|
return net;
|
|
} catch (err) {
|
|
const cached = await caches.match(req);
|
|
if (cached) return cached;
|
|
throw err;
|
|
}
|
|
})());
|
|
});
|