diff --git a/CHANGELOG.md b/CHANGELOG.md index e45e35e..a78d9d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi ## [Unreleased] ### Changed +- **Rehearser voice-design extraction.** Character development, backend selection, and backend-refresh synchronization + now live in `rehearser-voice-design.js` while preserving the existing cast controls and global API. - **Voice Design stylesheet split.** Voice-design form, sample-grid, and preview-match rules now live in `static/css/voice-design.css`. - **Background-job stylesheet split.** Job history and retry-panel rules now live in `static/css/background-jobs.css`. diff --git a/TODO.md b/TODO.md index 54d2f6d..73d2ce2 100644 --- a/TODO.md +++ b/TODO.md @@ -132,6 +132,7 @@ architecture change: the larger Audiobook/Rehearser/style split. - [x] Extract Rehearser voice-picker presentation and preview state into `rehearser-voice-picker.js`. - [x] Extract Rehearser cast view, sorting, filtering, and toolbar wiring into `rehearser-cast.js`. - [x] Extract Rehearser training transcript comparison helpers into `rehearser-train.js`. +- [x] Extract Rehearser character development and backend-selection controls into `rehearser-voice-design.js`. - [x] Extract Rehearser library persistence and IndexedDB migration into `rehearser-library.js`. - [ ] Extract Audiobook parsing and casting. - [x] Extract Audiobook parsing primitives and speaker-heuristic constants into `audiobook-parse.js`. diff --git a/VERSION b/VERSION index 68eebc0..fb56e1f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.24.36 +1.24.37 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 577a6c9..97273d7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -34,6 +34,7 @@ Done incrementally as global-scope ordered files (no ES-module change needed) - ✅ `rehearser-stage.js` — stage font/cast sidebar controls and cast-strip rendering; playback and transport remain in `rehearser.js`. - ✅ `rehearser-voice-picker.js` — voice preview state and matched-choice presentation; import and assignment actions remain in `rehearser.js`. - ✅ `rehearser-cast.js` — cast view, sort/filter state handlers, and cast toolbar wiring; card rendering remains in `rehearser.js`. +- ✅ `rehearser-voice-design.js` — character development, backend selection, and backend-refresh synchronization. - ✅ `audiobook-parse.js` — dialogue detection, dehyphenation, and speaker-heuristic constants; the casting pipeline remains in `audiobook.js`. - ✅ `audiobook-cast.js` — cast Markdown export and legacy page-stamping migration helpers; live casting remains in `audiobook.js`. - ✅ `static/css/resource-diagnostics.css` — isolated Performance/resource diagnostic panel styles. diff --git a/static/index.html b/static/index.html index 3aad0d1..ee6ceec 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,10 +27,10 @@ - - - - + + + + @@ -395,7 +395,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/rehearser-voice-design.js b/static/js/rehearser-voice-design.js new file mode 100644 index 0000000..6a67feb --- /dev/null +++ b/static/js/rehearser-voice-design.js @@ -0,0 +1,69 @@ +// ── Rehearser voice design controls ───────────────────────────────────────── +// Character development and backend selection resolve the shared Rehearser +// state at interaction time, so they can be loaded as a leaf feature module. + +async function _castDevelop(sp, btn) { + const script = $('reh-script-text')?.value.trim() || linesToScriptText(); + if (!script) { toast('Load a script first', 'error'); return; } + const c = rehState.cast[sp]; + const orig = btn.innerHTML; + btn.disabled = true; btn.innerHTML = ' Developing…'; + try { + const r = await fetch('/api/analyze-characters', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + script, names: [sp], + llm_url: $('reh-llm-url')?.value.trim() || rehDefaultLlmUrl(), + model: $('reh-llm-model')?.value || '', + language: c.lang || $('reh-design-lang')?.value || 'English', + }), + }); + if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.detail || r.statusText); } + const info = (await r.json()).characters?.[0] || {}; + if (info.gender) c.gender = String(info.gender).toUpperCase().charAt(0).replace(/[^MFN]/, 'N'); + if (info.description) { c.soul = info.description; c.instruct = c.instruct || info.description; } + renderCastList(); + toast(`Developed ${sp}`, 'success'); + } catch (e) { + btn.disabled = false; btn.innerHTML = orig; + toast('Develop failed: ' + e.message, 'error'); + } +} + +async function refreshRehBackends() { + rehInitLlmField(); + const sel = $('reh-backend-select'); if (!sel) return; + let backends = typeof availableTtsBackends === 'function' ? availableTtsBackends() : []; + if (!backends.length && typeof refreshTtsBackendAvailability === 'function') { + await refreshTtsBackendAvailability().catch(() => {}); + backends = typeof availableTtsBackends === 'function' ? availableTtsBackends() : []; + } + if (!backends.length && typeof _ttsBackends !== 'undefined' && Array.isArray(_ttsBackends)) backends = _ttsBackends; + const prev = sel.value || rehState.backend; + sel.innerHTML = backends.length + ? backends.map(b => ``).join('') + : ''; + if (prev && [...sel.options].some(o => o.value === prev)) sel.value = prev; + else { + const preferred = ['fishspeech', 'voice_clone', 'customvoice', 'voice_design']; + const pick = preferred.find(id => [...sel.options].some(o => o.value === id)); + if (pick) sel.value = pick; + } + rehState.backend = sel.value || ''; + _checkToneStyleSupport(); +} + +(window._ttsRefreshHooks = window._ttsRefreshHooks || []).push(() => { + const sel = $('reh-backend-select'); if (!sel) return; + const backends = typeof availableTtsBackends === 'function' ? availableTtsBackends() : []; + if (!backends.length) return; + const prev = sel.value || rehState.backend; + sel.innerHTML = backends.map(b => ``).join(''); + if (prev && [...sel.options].some(o => o.value === prev)) sel.value = prev; + else if (sel.options.length) sel.value = sel.options[0].value; + rehState.backend = sel.value || ''; +}); + +window.TTVC = window.TTVC || {}; +window.TTVC.rehearser = window.TTVC.rehearser || {}; +window.TTVC.rehearser.voiceDesign = { develop: _castDevelop, refreshBackends: refreshRehBackends }; diff --git a/static/js/rehearser.js b/static/js/rehearser.js index 4018b3f..7a71f30 100644 --- a/static/js/rehearser.js +++ b/static/js/rehearser.js @@ -1531,80 +1531,6 @@ function _castDeleteCharacter(sp) { toast(`Removed ${sp}`, 'success'); } -// Ask the LLM to flesh out one character → fills the soul brief + design prompt -async function _castDevelop(sp, btn) { - const script = $('reh-script-text')?.value.trim() || linesToScriptText(); - if (!script) { toast('Load a script first', 'error'); return; } - const c = rehState.cast[sp]; - const orig = btn.innerHTML; - btn.disabled = true; btn.innerHTML = ' Developing…'; - try { - const r = await fetch('/api/analyze-characters', { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - script, names: [sp], - llm_url: $('reh-llm-url')?.value.trim() || rehDefaultLlmUrl(), - model: $('reh-llm-model')?.value || '', - language: c.lang || $('reh-design-lang')?.value || 'English', - }), - }); - if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.detail || r.statusText); } - const info = (await r.json()).characters?.[0] || {}; - if (info.gender) c.gender = String(info.gender).toUpperCase().charAt(0).replace(/[^MFN]/, 'N'); - if (info.description) { c.soul = info.description; c.instruct = c.instruct || info.description; } - renderCastList(); - toast(`Developed ${sp}`, 'success'); - } catch (e) { - btn.disabled = false; btn.innerHTML = orig; - toast('Develop failed: ' + e.message, 'error'); - } -} - -async function refreshRehBackends() { - rehInitLlmField(); - const sel = $('reh-backend-select'); if (!sel) return; - let backends = typeof availableTtsBackends === 'function' ? availableTtsBackends() : []; - // If the global list hasn't been probed yet, trigger the probe now - if (!backends.length && typeof refreshTtsBackendAvailability === 'function') { - await refreshTtsBackendAvailability().catch(() => {}); - backends = typeof availableTtsBackends === 'function' ? availableTtsBackends() : []; - } - // Last resort: show all known backends (even unavailable ones) so the user can try - if (!backends.length && typeof _ttsBackends !== 'undefined' && Array.isArray(_ttsBackends)) { - backends = _ttsBackends; - } - const prev = sel.value || rehState.backend; - sel.innerHTML = backends.length - ? backends.map(b => ``).join('') - : ''; - if (prev && [...sel.options].some(o => o.value === prev)) { - // Restore a previously chosen backend - sel.value = prev; - } else { - // Default backend preference. fishspeech is best — it clones each voice's saved WAV - // (consistent identity) AND honours per-line emotion markers (tone), so a character - // sounds the same across the rehearsal while still reacting to tone. voice_clone is the - // fallback for consistency (weak tone); voice_design gives tone but drifts each line. - const preferred = ['fishspeech', 'voice_clone', 'customvoice', 'voice_design']; - const pick = preferred.find(id => [...sel.options].some(o => o.value === id)); - if (pick) sel.value = pick; - } - rehState.backend = sel.value || ''; // keep state in sync with the (possibly auto-picked) value - _checkToneStyleSupport(); -} - -// Keep the rehearser select in sync whenever the global backend list refreshes -(window._ttsRefreshHooks = window._ttsRefreshHooks || []).push(() => { - const sel = $('reh-backend-select'); if (!sel) return; - const backends = typeof availableTtsBackends === 'function' ? availableTtsBackends() : []; - if (!backends.length) return; - const prev = sel.value || rehState.backend; - sel.innerHTML = backends.map(b => ``).join(''); - if (prev && [...sel.options].some(o => o.value === prev)) sel.value = prev; - else if (sel.options.length) sel.value = sel.options[0].value; - rehState.backend = sel.value || ''; -}); - // All voice IDs offered in the cast/narrator pickers — backend voices PLUS everything // in the library, so any saved/cloned/imported voice (incl. for the narrator) is pickable. function _rehAllVoiceIds(include) { diff --git a/static/module-manifest.json b/static/module-manifest.json index 97095ed..0ad2965 100644 --- a/static/module-manifest.json +++ b/static/module-manifest.json @@ -4,9 +4,9 @@ "integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace" ], "lazy": { - "s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"], - "s-reader": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-cast", "audiobook", "character-sheets"], - "s-library": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-cast", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"], - "s-caststudio": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-cast", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"] + "s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"], + "s-reader": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-cast", "audiobook", "character-sheets"], + "s-library": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-cast", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"], + "s-caststudio": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-cast", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"] } }