refactor: extract rehearser voice design
This commit is contained in:
parent
d43959728d
commit
2fde0a176d
@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### 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
|
- **Voice Design stylesheet split.** Voice-design form, sample-grid, and preview-match rules now live in
|
||||||
`static/css/voice-design.css`.
|
`static/css/voice-design.css`.
|
||||||
- **Background-job stylesheet split.** Job history and retry-panel rules now live in `static/css/background-jobs.css`.
|
- **Background-job stylesheet split.** Job history and retry-panel rules now live in `static/css/background-jobs.css`.
|
||||||
|
|||||||
1
TODO.md
1
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 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 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 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`.
|
- [x] Extract Rehearser library persistence and IndexedDB migration into `rehearser-library.js`.
|
||||||
- [ ] Extract Audiobook parsing and casting.
|
- [ ] Extract Audiobook parsing and casting.
|
||||||
- [x] Extract Audiobook parsing primitives and speaker-heuristic constants into `audiobook-parse.js`.
|
- [x] Extract Audiobook parsing primitives and speaker-heuristic constants into `audiobook-parse.js`.
|
||||||
|
|||||||
@ -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-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-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-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-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`.
|
- ✅ `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.
|
- ✅ `static/css/resource-diagnostics.css` — isolated Performance/resource diagnostic panel styles.
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<meta name="theme-color" content="#2563EB">
|
<meta name="theme-color" content="#2563EB">
|
||||||
<meta name="app-version" content="1.24.36">
|
<meta name="app-version" content="1.24.37">
|
||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||||
@ -27,10 +27,10 @@
|
|||||||
|
|
||||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||||
<link rel="stylesheet" href="/static/style.css?v=1.24.36">
|
<link rel="stylesheet" href="/static/style.css?v=1.24.37">
|
||||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.36">
|
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.37">
|
||||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.36">
|
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.37">
|
||||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.36">
|
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.37">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -395,7 +395,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||||
<script src="/static/loader.js?v=1.24.36"></script>
|
<script src="/static/loader.js?v=1.24.37"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
69
static/js/rehearser-voice-design.js
Normal file
69
static/js/rehearser-voice-design.js
Normal file
@ -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 = '<span class="reh-imsdb-spinner"></span> 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 => `<option value="${escHtml(b.id)}">${escHtml(b.label)}</option>`).join('')
|
||||||
|
: '<option value="">No backend available</option>';
|
||||||
|
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 => `<option value="${escHtml(b.id)}">${escHtml(b.label)}</option>`).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 };
|
||||||
@ -1531,80 +1531,6 @@ function _castDeleteCharacter(sp) {
|
|||||||
toast(`Removed ${sp}`, 'success');
|
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 = '<span class="reh-imsdb-spinner"></span> 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 => `<option value="${escHtml(b.id)}">${escHtml(b.label)}</option>`).join('')
|
|
||||||
: '<option value="">No backend available</option>';
|
|
||||||
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 => `<option value="${escHtml(b.id)}">${escHtml(b.label)}</option>`).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
|
// 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.
|
// in the library, so any saved/cloned/imported voice (incl. for the narrator) is pickable.
|
||||||
function _rehAllVoiceIds(include) {
|
function _rehAllVoiceIds(include) {
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
"integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace"
|
"integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace"
|
||||||
],
|
],
|
||||||
"lazy": {
|
"lazy": {
|
||||||
"s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"],
|
"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", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-cast", "audiobook", "character-sheets"],
|
"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", "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-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", "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-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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user