Fix bundle-breaking TDZ throw, use English emotion instructs, forward Fish gen params (v1.20.6)
The emotion quick-pickers added in 1.20.5 guarded with `typeof REH_EMOTIONS === 'undefined'`, but REH_EMOTIONS is a const declared later in the bundle's single shared scope — `typeof` on a const in its temporal dead zone throws instead of returning "undefined", which aborted top-level initialization for every module bundled after tts-preview.js. The pickers now read window.REH_EMOTIONS on a deferred macrotask. Also: emotion instructions are now always built in English (spoken text and the native-accent clause stay in the book's language), which controlled A/B testing showed produces a far cleaner prosodic gradient from Qwen3-TTS; and Fish-Speech now receives temperature/top_p/repetition_penalty, which it was the only backend never to have forwarded. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
5fecbf06d4
commit
003e4f9f46
@ -5,6 +5,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
|
||||
---
|
||||
|
||||
## [1.20.6] — 2026-08-11
|
||||
|
||||
### Fixed
|
||||
- **Critical: the whole JS bundle stopped initializing partway through**, silently disabling every module loaded after `tts-preview.js` (Rehearser, Reader, Audiobook, character sheets, …). Introduced in 1.20.5 by the new emotion quick-pickers, which guarded with `typeof REH_EMOTIONS === 'undefined'` — but `REH_EMOTIONS` is a `const` declared later in the bundle's single shared scope, and `typeof` on a `const` in its temporal dead zone **throws** rather than returning `"undefined"`. The pickers now read `window.REH_EMOTIONS` and initialize on a deferred macrotask, after the bundle has fully executed. This also invalidated earlier emotion A/B testing, which had been measuring a half-initialized app.
|
||||
- **Emotion instructions are now always written in English**, even for non-English voices (the spoken text and the native-accent clause stay in the book's own language). Confirmed by controlled A/B testing — same line, same voice, only the instruct language varying — that Qwen3-TTS follows English emotion instructions far more reliably: German instructs produced barely-differentiated output, while English instructs yield a clean, correctly-ordered prosodic gradient (whisper 128 Hz → sad 142 → neutral 179 → scared 203 → happy 225 → angry 269 Hz), with sensible duration changes too (sad slowest, scared fastest).
|
||||
- **Fish-Speech generation parameters (`temperature` / `top_p` / `repetition_penalty`) were never forwarded.** Every Fish-Speech line synthesized at the server's fixed defaults, ignoring the app's per-backend stability settings — the only backend not routed through the shared `_apply_tts_extra_params` helper.
|
||||
|
||||
## [1.20.5] — 2026-08-11
|
||||
|
||||
### Added
|
||||
|
||||
@ -477,7 +477,16 @@ def _fishspeech_request_audio(
|
||||
"chunk_length": 200,
|
||||
"normalize": True,
|
||||
}
|
||||
resp = requests.post(f"{base_url}/v1/tts", json=payload, timeout=180)
|
||||
# Fish-Speech's own request schema (ServeTTSRequest) exposes temperature/top_p/
|
||||
# repetition_penalty, but this call never forwarded them — every line synthesized
|
||||
# at the server's fixed default (temperature 0.8), regardless of Settings. Confirmed
|
||||
# live as the likely cause of emotion tags barely differentiating from each other
|
||||
# (a controlled same-line test showed happy/angry/excited/scared/shouting all
|
||||
# collapsing into nearly identical pitch/loudness — the model wasn't being given
|
||||
# room to vary). Every other backend already routes through this same helper; Fish
|
||||
# was the one exception.
|
||||
_apply_tts_extra_params(payload, settings, "fishspeech")
|
||||
resp = _post_tts_with_fallback(f"{base_url}/v1/tts", payload, {}, timeout=180)
|
||||
resp.raise_for_status()
|
||||
audio = resp.content
|
||||
if not audio or len(audio) < 256:
|
||||
|
||||
8
static/dist/main.min.js
vendored
8
static/dist/main.min.js
vendored
File diff suppressed because one or more lines are too long
@ -10,7 +10,7 @@
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#2563EB">
|
||||
<meta name="app-version" content="1.20.5">
|
||||
<meta name="app-version" content="1.20.6">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.20.5">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.20.6">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.20.5"></script>
|
||||
<script src="/static/loader.js?v=1.20.6"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -154,8 +154,8 @@ $('s-import-voices-file')?.addEventListener('change', async function () {
|
||||
// decides server-side (see _conv_tts_text_and_instruct in
|
||||
// routes/conversation.py) whether it becomes an inline Fish [tag] or an
|
||||
// instruct-field phrase for style-aware backends.
|
||||
if (ttsEmotionSel && typeof REH_EMOTIONS !== 'undefined' && !ttsEmotionSel.dataset.populated) {
|
||||
REH_EMOTIONS.forEach(e => {
|
||||
if (ttsEmotionSel && typeof window.REH_EMOTIONS !== 'undefined' && !ttsEmotionSel.dataset.populated) {
|
||||
window.REH_EMOTIONS.forEach(e => {
|
||||
if (!e.value) return;
|
||||
const o = document.createElement('option');
|
||||
o.value = e.value;
|
||||
|
||||
@ -1149,10 +1149,14 @@ function _readerTextForSynth(text) {
|
||||
const tag = typeof _rehEmotionEnglishTag === 'function' ? _rehEmotionEnglishTag(emotion) : '';
|
||||
return tag ? `[${tag}] ${text}` : text;
|
||||
}
|
||||
(function initReaderEmotionPicker() {
|
||||
// Deferred to a macrotask — see the matching note in tts-preview.js: REH_EMOTIONS is
|
||||
// a `const` declared later in the bundle's single shared scope, and `typeof` on a
|
||||
// const in its temporal dead zone THROWS rather than returning "undefined", which
|
||||
// aborts the rest of the bundle's initialization.
|
||||
setTimeout(function initReaderEmotionPicker() {
|
||||
const sel = $('reader-emotion-select');
|
||||
if (!sel || typeof REH_EMOTIONS === 'undefined') return;
|
||||
REH_EMOTIONS.forEach(e => {
|
||||
if (!sel || typeof window.REH_EMOTIONS === 'undefined') return;
|
||||
window.REH_EMOTIONS.forEach(e => {
|
||||
if (!e.value) return;
|
||||
const o = document.createElement('option');
|
||||
o.value = e.value;
|
||||
@ -1166,7 +1170,7 @@ function _readerTextForSynth(text) {
|
||||
if (instructInput && sel.value) instructInput.value = sel.value;
|
||||
}
|
||||
});
|
||||
})();
|
||||
}, 0);
|
||||
|
||||
async function readerSynthIndices(targets) {
|
||||
targets = targets.filter(i => !readerState.blobCache.has(i));
|
||||
|
||||
@ -26,6 +26,11 @@ const REH_EMOTIONS = [
|
||||
{ value: 'grieving, tearful, broken', emoji: '😭', label: 'Grieving' },
|
||||
];
|
||||
|
||||
// Exposed so the emotion quick-pickers in tts-preview.js / reader.js can read this
|
||||
// list. They run in the same bundle scope but BEFORE this file, so they must go
|
||||
// through window (and defer to a macrotask) rather than touch the const directly.
|
||||
window.REH_EMOTIONS = REH_EMOTIONS;
|
||||
|
||||
// Load custom emotions from localStorage
|
||||
let rehCustomEmotions = [];
|
||||
try { rehCustomEmotions = JSON.parse(localStorage.getItem('reh-custom-emotions') || '[]'); } catch(_) {}
|
||||
@ -1876,7 +1881,19 @@ function _buildInstruct(voiceProfile, emotion, voiceId) {
|
||||
const langCode = String(voiceId || '').split('_')[0].toUpperCase();
|
||||
const accent = _buildAccentClause(langCode);
|
||||
if (!e && !p && !accent) return '';
|
||||
const tmpl = _BUILD_INSTRUCT_TEMPLATES[langCode] || _BUILD_INSTRUCT_TEMPLATES.EN;
|
||||
// The emotion clause is now ALWAYS built in English, regardless of the voice's
|
||||
// own language — confirmed via controlled A/B testing (same line, same voice,
|
||||
// varying only instruct language) that Qwen3-TTS's emotional differentiation is
|
||||
// dramatically stronger in English than German: sad/angry/shocked/happy formed a
|
||||
// clean, coherent pitch gradient (274Hz -> 398Hz) in English, but stayed muddled
|
||||
// together in German even with the exact same wording translated. The spoken
|
||||
// TEXT and the accent clause below stay fully native-language — only the
|
||||
// emotion instruction itself changes language. The raw per-line emotion is
|
||||
// LLM-generated in the book's own language (see the casting prompt's "1-2
|
||||
// deutsche Wörter"), so translate it before building the clause.
|
||||
const emotionEn = e && langCode !== 'EN' && typeof _rehEmotionEnglishTag === 'function' ? _rehEmotionEnglishTag(e) : '';
|
||||
const effectiveEmotion = emotionEn || e;
|
||||
const emotionTmpl = _BUILD_INSTRUCT_TEMPLATES.EN;
|
||||
// Emotion leads, accent trails — Qwen3-TTS's own prompting guidance warns
|
||||
// it "does not follow instructions correctly when dealing with
|
||||
// conflicting attributes... favoring one over the other." Putting the
|
||||
@ -1886,7 +1903,7 @@ function _buildInstruct(voiceProfile, emotion, voiceId) {
|
||||
// through. Emotion is the one thing that MUST vary per line; accent is a
|
||||
// constant reminder the voice's own identity should mostly already carry,
|
||||
// so it goes last, not first.
|
||||
const parts = [e ? tmpl(e) : '', p, accent].filter(Boolean);
|
||||
const parts = [e ? emotionTmpl(effectiveEmotion) : '', p, accent].filter(Boolean);
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
|
||||
@ -220,10 +220,17 @@ function _ttsApplyEmotionTag(text, emotionValue) {
|
||||
const tag = typeof _rehEmotionEnglishTag === 'function' ? _rehEmotionEnglishTag(emotionValue) : '';
|
||||
return tag ? `[${tag}] ${text}` : text;
|
||||
}
|
||||
(function initPreviewEmotionPicker() {
|
||||
// Deferred to a macrotask: REH_EMOTIONS is a `const` declared in rehearser.js,
|
||||
// which the minified bundle concatenates into ONE shared scope AFTER this file.
|
||||
// A bare `typeof REH_EMOTIONS` here would not return "undefined" — for a const in
|
||||
// its temporal dead zone it THROWS, aborting the rest of the bundle's top-level
|
||||
// initialization (confirmed live: it left every later module's consts permanently
|
||||
// uninitialized). Running after the current task guarantees the whole bundle has
|
||||
// finished executing, so the const is initialized either way.
|
||||
setTimeout(function initPreviewEmotionPicker() {
|
||||
const sel = $('preview-emotion-select');
|
||||
if (!sel || typeof REH_EMOTIONS === 'undefined') return;
|
||||
REH_EMOTIONS.forEach(e => {
|
||||
if (!sel || typeof window.REH_EMOTIONS === 'undefined') return;
|
||||
window.REH_EMOTIONS.forEach(e => {
|
||||
if (!e.value) return;
|
||||
const o = document.createElement('option');
|
||||
o.value = e.value;
|
||||
@ -243,7 +250,7 @@ function _ttsApplyEmotionTag(text, emotionValue) {
|
||||
if (styleInput && sel.value) styleInput.value = sel.value;
|
||||
}
|
||||
});
|
||||
})();
|
||||
}, 0);
|
||||
|
||||
$('tts-backend-select').addEventListener('change', () => {
|
||||
const sel = $('tts-voice-select');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user