// ── Library Characters / Cast workspace ───────────────────────────────────── // Loaded after library.js; uses its shared production helpers and exports // libraryRenderCharacters for the Library section router. // ── Characters / Cast ───────────────────────────────────────────────────────── let _libCharsRenderSeq = 0; async function libraryRenderCharacters() { const container = document.getElementById('lib-chars-list'); if (!container) return; const renderSeq = ++_libCharsRenderSeq; // This page can open before the shared voice library has finished loading. // Ratings live in window._voices, so rendering during that gap makes every // assigned voice look unrated even when its .meta.json still has a rating. if ((!Array.isArray(window._voices) || !window._voices.length) && typeof loadVoiceLibrary === 'function') { try { await loadVoiceLibrary(); } catch (e) { console.warn('[characters] voice library load failed', e); } if (renderSeq !== _libCharsRenderSeq) return; } // Fetch BEFORE touching the DOM: a transient failure here (e.g. the // server briefly unresponsive while a TTS container restart is in // flight) must never blank out an already-populated list — confirmed // live as "No characters yet" flashing up over a real, populated cast // while a bulk voice-design run elsewhere was mid-restart. Leave whatever // is already on screen alone and let the next real refresh fix it. let all = []; try { all = (typeof clGetAll === 'function') ? await clGetAll() : []; } catch (e) { console.warn('[characters] load failed', e); if (typeof toast === 'function') toast('Failed to load characters — keeping the current view', 'error'); return; } if (renderSeq !== _libCharsRenderSeq) return; // Every action on this page (remove voice, auto-design, delete...) re-runs // this full rebuild via `refresh()`. Collapsing the container down to a // one-line loading placeholder mid-rebuild shrinks #main-content below the // user's current scroll position, which the browser clamps back to fit — // confirmed live as "the screen jumps to the top" after every single // action. Restore it once the real content is back, unless a caller // explicitly wants to jump to a specific book (see _libCharsScrollToBook // below), which takes priority over just staying put. const mainEl = document.getElementById('main-content'); const savedScrollTop = (!window._libCharsScrollToBook && mainEl) ? mainEl.scrollTop : null; container.innerHTML = '
No characters yet.
' + 'Open a book in Read Aloud, cast it as an audiobook, then click Cast Characters to generate character sheets — or import an existing cast from SillyTavern.
' + '| ' + th('alpha', 'Name') + th('gender', 'Geschlecht') + th('age', 'Alter', 'Estimated age') + th('lines', 'Zeilen', 'Anzahl Zeilen') + th('language', 'Sprache') + th('align', 'Gut/Böse', 'Moralische Gesinnung') + th('profile', 'Profil (gefüllt)', 'Sort by filled profile fields; the second number is always 32') + th('voice', 'Stimme') + th('rating', 'Rating', 'Rate the assigned voice') + ' | Voice Design Prompt | Notes | ' + 'Tags | Book / Script | ' + ' |
|---|
Used in every voice design (and image) prompt for this book, so a fantasy story doesn\'t end up with 1920s-general portraits or English voices in a German book just because one character\'s own sheet was too sparse to tell.
' + '"' + escHtml(reuse.voiceId) + '" is already used for ' + escHtml(rec.name) + ' in "' + escHtml(reuse.book) + '". Reuse it for series consistency, or design a brand-new voice just for this book?
' + 'Edit the description, then generate a new voice from it. This replaces ' + (rec.voice ? 'the current voice' : 'this character’s voice') + '.
' + '' + '