Prefer the newest casting draft instead of always trusting localStorage (v1.20.22)

Opening a book always restored the localStorage draft and then pushed it to the
server as a backfill, so a tab holding an older copy silently overwrote newer
work saved from anywhere else — confirmed live: a cast improved from 79 Unknown
lines to 25 was destroyed by opening the book in a stale tab. Local and server
drafts are now compared by savedAt, the newest wins and is cached locally, and
the server is only backfilled when the local copy is actually newer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-08-12 10:29:53 +02:00
parent 56800dab28
commit 3434293375
5 changed files with 32 additions and 16 deletions

View File

@ -22,6 +22,11 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
- **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.22] — 2026-08-12
### Fixed
- **A stale browser draft could silently destroy newer casting work.** On opening a book the localStorage draft always won and was then pushed to the server as a "backfill", so a tab holding an older copy overwrote whatever was stored server-side — confirmed live: a cast improved from 79 Unknown lines to 25 was wiped simply by opening the book in a tab that still held the old draft. The two copies are now compared by their save timestamp, the newest wins (and is cached locally), and the server is only backfilled when the local copy is genuinely the newer one.
## [1.20.21] — 2026-08-12
### Fixed

View File

@ -1 +1 @@
1.20.21
1.20.22

File diff suppressed because one or more lines are too long

View File

@ -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.21">
<meta name="app-version" content="1.20.22">
<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.21">
<link rel="stylesheet" href="/static/style.css?v=1.20.22">
<!-- ── 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.21"></script>
<script src="/static/loader.js?v=1.20.22"></script>
</body>
</html>

View File

@ -4808,21 +4808,32 @@ async function audiobookOpenCastView() {
view.complete(summary, audiobookShowPreview, audiobookCast, audiobookRecastUnknown);
};
// 2. localStorage draft restore — fastest, no network
// 2. localStorage draft restore — fastest, but NOT automatically authoritative.
// The local copy used to win outright and was then pushed to the server as a
// "backfill", which meant a stale browser draft silently overwrote newer work
// saved from anywhere else — confirmed live: a cast improved from 79 Unknown
// lines to 25 and stored server-side was destroyed simply by opening the book
// in a tab holding the older draft. Whichever copy was saved most recently
// wins, and the server is only backfilled when the local copy is genuinely
// newer.
const _localDraft = _abLoadDraft(text);
if (_localDraft && _localDraft.segments && _localDraft.segments.length > 0) {
const view = audiobookCastView(chunks.length, llm_url, model, false);
_applyDraft(_localDraft, view, 'local');
// Backfill server in case it's missing this draft
if (_abBookId()) {
const serverCopy = {
..._localDraft,
bookId: _abBookId(),
title: window.readerState?.title || _localDraft.title || '',
};
fetch(`/api/reader/docs/${encodeURIComponent(_abBookId())}/scripts/cast`, {
const bookId = _abBookId();
let chosen = _localDraft, source = 'local', serverDraft = null;
if (bookId) {
try { serverDraft = await _abLoadDraftServer(bookId); } catch (_) {}
if (serverDraft && (serverDraft.savedAt || 0) > (_localDraft.savedAt || 0)) {
chosen = serverDraft; source = 'server';
try { localStorage.setItem(_abDraftKey(bookId), JSON.stringify(serverDraft)); } catch (_) {}
}
}
_applyDraft(chosen, view, source);
// Only push upwards when the local copy really is the newer one.
if (bookId && source === 'local' && (!serverDraft || (_localDraft.savedAt || 0) > (serverDraft.savedAt || 0))) {
fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast`, {
method: 'PUT', headers: {'Content-Type':'application/json'},
body: JSON.stringify(serverCopy)
body: JSON.stringify({ ..._localDraft, bookId, title: window.readerState?.title || _localDraft.title || '' })
}).catch(() => {});
}
return;