// ── Library Characters / Cast workspace ───────────────────────────────────── // Loaded after library.js; uses its shared production helpers and exports // libraryRenderCharacters for the Library section router. // ── Characters / Cast ───────────────────────────────────────────────────────── (function (root) { let _libCharsRenderSeq = 0; async function _libReviewVisualPairs(button, container) { if (!button) return; button.disabled = true; const oldLabel = button.innerHTML; button.innerHTML = ' Reviewing…'; try { const response = await fetch('/api/characters/visual-pair-review'); const payload = await response.json(); if (!response.ok) throw new Error(payload.detail || 'Visual-pair review failed'); let panel = container.querySelector('#lib-chars-visual-review'); if (!panel) { panel = document.createElement('div'); panel.id = 'lib-chars-visual-review'; panel.className = 'lib-chars-visual-review'; container.insertBefore(panel, container.firstChild); } if (!payload.issues || !payload.issues.length) { panel.innerHTML = ' Visual pairs passed the metadata and asset-reuse checks (' + payload.checked + ' characters reviewed).'; if (typeof toast === 'function') toast('Visual pair review passed', 'success'); } else { panel.innerHTML = ' Review ' + payload.issues.length + ' character pair' + (payload.issues.length === 1 ? '' : 's') + ': ' + payload.issues.map(function (item) { return '' + escHtml(item.name || item.id) + ' — ' + escHtml(item.issues.join(', ')) + ''; }).join(''); if (typeof toast === 'function') toast(payload.issues.length + ' visual pair(s) need review', 'warning'); } } catch (error) { if (typeof toast === 'function') toast(error.message || 'Visual-pair review failed', 'error'); } finally { button.disabled = false; button.innerHTML = oldLabel; } } 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') + '.
' + '' + '