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
|
## [1.20.5] — 2026-08-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -477,7 +477,16 @@ def _fishspeech_request_audio(
|
|||||||
"chunk_length": 200,
|
"chunk_length": 200,
|
||||||
"normalize": True,
|
"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()
|
resp.raise_for_status()
|
||||||
audio = resp.content
|
audio = resp.content
|
||||||
if not audio or len(audio) < 256:
|
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="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.20.5">
|
<meta name="app-version" content="1.20.6">
|
||||||
<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,7 +27,7 @@
|
|||||||
|
|
||||||
<!-- ── 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.20.5">
|
<link rel="stylesheet" href="/static/style.css?v=1.20.6">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -378,7 +378,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.20.5"></script>
|
<script src="/static/loader.js?v=1.20.6"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -154,8 +154,8 @@ $('s-import-voices-file')?.addEventListener('change', async function () {
|
|||||||
// decides server-side (see _conv_tts_text_and_instruct in
|
// decides server-side (see _conv_tts_text_and_instruct in
|
||||||
// routes/conversation.py) whether it becomes an inline Fish [tag] or an
|
// routes/conversation.py) whether it becomes an inline Fish [tag] or an
|
||||||
// instruct-field phrase for style-aware backends.
|
// instruct-field phrase for style-aware backends.
|
||||||
if (ttsEmotionSel && typeof REH_EMOTIONS !== 'undefined' && !ttsEmotionSel.dataset.populated) {
|
if (ttsEmotionSel && typeof window.REH_EMOTIONS !== 'undefined' && !ttsEmotionSel.dataset.populated) {
|
||||||
REH_EMOTIONS.forEach(e => {
|
window.REH_EMOTIONS.forEach(e => {
|
||||||
if (!e.value) return;
|
if (!e.value) return;
|
||||||
const o = document.createElement('option');
|
const o = document.createElement('option');
|
||||||
o.value = e.value;
|
o.value = e.value;
|
||||||
|
|||||||
@ -1149,10 +1149,14 @@ function _readerTextForSynth(text) {
|
|||||||
const tag = typeof _rehEmotionEnglishTag === 'function' ? _rehEmotionEnglishTag(emotion) : '';
|
const tag = typeof _rehEmotionEnglishTag === 'function' ? _rehEmotionEnglishTag(emotion) : '';
|
||||||
return tag ? `[${tag}] ${text}` : text;
|
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');
|
const sel = $('reader-emotion-select');
|
||||||
if (!sel || typeof REH_EMOTIONS === 'undefined') return;
|
if (!sel || typeof window.REH_EMOTIONS === 'undefined') return;
|
||||||
REH_EMOTIONS.forEach(e => {
|
window.REH_EMOTIONS.forEach(e => {
|
||||||
if (!e.value) return;
|
if (!e.value) return;
|
||||||
const o = document.createElement('option');
|
const o = document.createElement('option');
|
||||||
o.value = e.value;
|
o.value = e.value;
|
||||||
@ -1166,7 +1170,7 @@ function _readerTextForSynth(text) {
|
|||||||
if (instructInput && sel.value) instructInput.value = sel.value;
|
if (instructInput && sel.value) instructInput.value = sel.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
}, 0);
|
||||||
|
|
||||||
async function readerSynthIndices(targets) {
|
async function readerSynthIndices(targets) {
|
||||||
targets = targets.filter(i => !readerState.blobCache.has(i));
|
targets = targets.filter(i => !readerState.blobCache.has(i));
|
||||||
|
|||||||
@ -26,6 +26,11 @@ const REH_EMOTIONS = [
|
|||||||
{ value: 'grieving, tearful, broken', emoji: '😭', label: 'Grieving' },
|
{ 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
|
// Load custom emotions from localStorage
|
||||||
let rehCustomEmotions = [];
|
let rehCustomEmotions = [];
|
||||||
try { rehCustomEmotions = JSON.parse(localStorage.getItem('reh-custom-emotions') || '[]'); } catch(_) {}
|
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 langCode = String(voiceId || '').split('_')[0].toUpperCase();
|
||||||
const accent = _buildAccentClause(langCode);
|
const accent = _buildAccentClause(langCode);
|
||||||
if (!e && !p && !accent) return '';
|
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
|
// Emotion leads, accent trails — Qwen3-TTS's own prompting guidance warns
|
||||||
// it "does not follow instructions correctly when dealing with
|
// it "does not follow instructions correctly when dealing with
|
||||||
// conflicting attributes... favoring one over the other." Putting the
|
// 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
|
// 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,
|
// constant reminder the voice's own identity should mostly already carry,
|
||||||
// so it goes last, not first.
|
// 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(' ');
|
return parts.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -220,10 +220,17 @@ function _ttsApplyEmotionTag(text, emotionValue) {
|
|||||||
const tag = typeof _rehEmotionEnglishTag === 'function' ? _rehEmotionEnglishTag(emotionValue) : '';
|
const tag = typeof _rehEmotionEnglishTag === 'function' ? _rehEmotionEnglishTag(emotionValue) : '';
|
||||||
return tag ? `[${tag}] ${text}` : text;
|
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');
|
const sel = $('preview-emotion-select');
|
||||||
if (!sel || typeof REH_EMOTIONS === 'undefined') return;
|
if (!sel || typeof window.REH_EMOTIONS === 'undefined') return;
|
||||||
REH_EMOTIONS.forEach(e => {
|
window.REH_EMOTIONS.forEach(e => {
|
||||||
if (!e.value) return;
|
if (!e.value) return;
|
||||||
const o = document.createElement('option');
|
const o = document.createElement('option');
|
||||||
o.value = e.value;
|
o.value = e.value;
|
||||||
@ -243,7 +250,7 @@ function _ttsApplyEmotionTag(text, emotionValue) {
|
|||||||
if (styleInput && sel.value) styleInput.value = sel.value;
|
if (styleInput && sel.value) styleInput.value = sel.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
}, 0);
|
||||||
|
|
||||||
$('tts-backend-select').addEventListener('change', () => {
|
$('tts-backend-select').addEventListener('change', () => {
|
||||||
const sel = $('tts-voice-select');
|
const sel = $('tts-voice-select');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user