Fix audiobook stop casting state
This commit is contained in:
parent
14d3e12ec8
commit
32a3c838fe
@ -9,6 +9,14 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
|
||||
---
|
||||
|
||||
## [1.12.57] — 2026-06-30
|
||||
|
||||
### Fixed
|
||||
- **Audiobook stop-casting flow** — stopping a book-to-audiobook cast now keeps the casting panel open, aborts the active request cleanly, and preserves completed passages instead of dropping back to the empty Reader import view.
|
||||
- **Interrupted cast drafts** — partial audiobook casts now save their real completed chunk count so recovery no longer treats an interrupted cast as 100% complete.
|
||||
|
||||
---
|
||||
|
||||
## [1.12.56] — 2026-06-30
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -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.12.56">
|
||||
<meta name="app-version" content="1.12.57">
|
||||
<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.12.56">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.12.57">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -362,7 +362,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.12.56"></script>
|
||||
<script src="/static/loader.js?v=1.12.57"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -418,10 +418,26 @@ STRIKTE FORMAT- UND TEXTREGELN:
|
||||
}
|
||||
};
|
||||
|
||||
const setStoppingState = () => {
|
||||
const cancelBtn = panel.querySelector('#ab-cv-cancel');
|
||||
if (cancelBtn) {
|
||||
cancelBtn.disabled = true;
|
||||
cancelBtn.innerHTML = '<span class="mdi mdi-loading mdi-spin"></span> Stopping...';
|
||||
cancelBtn.title = 'Stopping the current casting request';
|
||||
}
|
||||
const status = panel.querySelector('#ab-cv-status-msg');
|
||||
if (status) {
|
||||
status.style.display = 'inline-block';
|
||||
status.textContent = 'Stopping... completed passages will stay saved.';
|
||||
}
|
||||
};
|
||||
|
||||
panel.querySelector('#ab-cv-cancel').addEventListener('click', () => {
|
||||
if (_audiobook.running) {
|
||||
_audiobook.cancel = true;
|
||||
setStoppingState();
|
||||
if (typeof _audiobook.abort === 'function') _audiobook.abort();
|
||||
return;
|
||||
}
|
||||
closePanel();
|
||||
});
|
||||
@ -1463,7 +1479,59 @@ ABSOLUTE REGELN:
|
||||
panel.classList.add('ab-castpanel-done');
|
||||
if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(false);
|
||||
},
|
||||
done() { closePanel(); },
|
||||
done(options = {}) {
|
||||
const stopped = options.stopped || _audiobook.cancel;
|
||||
if (!stopped) { closePanel(); return; }
|
||||
|
||||
this.clearProcessing();
|
||||
if (count) count.hidden = true;
|
||||
const castBar = panel.querySelector('.ab-castpanel-bar');
|
||||
if (castBar) castBar.hidden = true;
|
||||
feed.querySelector('.ab-skel-feed')?.remove();
|
||||
chars.querySelector('.ab-skel-chars')?.remove();
|
||||
|
||||
const message = options.message || 'Casting stopped before any passage completed.';
|
||||
if (!feed.querySelector('.ab-cv-row, .ab-cv-note, .ab-cv-divider, .ab-cv-pagemark')) {
|
||||
feed.innerHTML = '';
|
||||
const r = document.createElement('div');
|
||||
r.className = 'ab-cv-note';
|
||||
r.textContent = message;
|
||||
feed.appendChild(r);
|
||||
} else if (options.message) {
|
||||
const r = document.createElement('div');
|
||||
r.className = 'ab-cv-note';
|
||||
r.textContent = message;
|
||||
feed.appendChild(r);
|
||||
}
|
||||
if (!chars.querySelector('.ab-char-item')) {
|
||||
chars.innerHTML = '<span class="ab-cv-empty">No completed characters yet.</span>';
|
||||
}
|
||||
|
||||
const foot = panel.querySelector('#ab-cv-foot');
|
||||
const hasSegments = Array.isArray(_audiobook.segments) && _audiobook.segments.length > 0;
|
||||
foot.hidden = false;
|
||||
foot.innerHTML = `<span class="ab-cv-done"><span class="mdi mdi-stop-circle-outline"></span> ${escHtml(message)}</span><span style="flex:1"></span><button class="btn-secondary btn-sm" id="ab-cv-stopped-back" style="margin-right:8px;"><span class="mdi mdi-arrow-left"></span> Back to reader</button>${hasSegments ? '<button class="btn-secondary btn-sm" id="ab-cv-stopped-review" style="margin-right:8px;"><span class="mdi mdi-drama-masks"></span> Review cast</button>' : ''}<button class="btn-primary btn-sm" id="ab-cv-stopped-recast"><span class="mdi mdi-refresh"></span> Cast again</button>`;
|
||||
|
||||
foot.querySelector('#ab-cv-stopped-back')?.addEventListener('click', closePanel);
|
||||
foot.querySelector('#ab-cv-stopped-review')?.addEventListener('click', audiobookShowPreview);
|
||||
foot.querySelector('#ab-cv-stopped-recast')?.addEventListener('click', () => {
|
||||
const newPrompt = panel.querySelector('#ab-cv-prompt-text')?.value;
|
||||
const currentUrl = panel.querySelector('#ab-cv-llm-url')?.value.trim();
|
||||
const currentModel = panel.querySelector('#ab-cv-llm-select')?.value;
|
||||
if (typeof _appSettings !== 'undefined' && newPrompt) _appSettings.audiobook_prompt = newPrompt;
|
||||
fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ audiobook_prompt: newPrompt || '' })
|
||||
}).finally(() => {
|
||||
closePanel();
|
||||
audiobookCast(currentUrl, currentModel);
|
||||
});
|
||||
});
|
||||
|
||||
panel.classList.add('ab-castpanel-done');
|
||||
if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(false);
|
||||
},
|
||||
// Show fresh "Ready to cast" state (used when server draft fetch returns empty)
|
||||
setFreshState() {
|
||||
feed.querySelector('.ab-skel-feed')?.remove();
|
||||
@ -1523,7 +1591,14 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel) {
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') { _audiobook.running = false; view.done(); return; }
|
||||
if (err.name === 'AbortError') {
|
||||
_audiobook.cancel = true;
|
||||
_audiobook.running = false;
|
||||
_audiobook.abort = null;
|
||||
view.done({ stopped: true, message: 'Recast stopped before any lines were updated.' });
|
||||
toast('Recast stopped. Existing cast preserved.', 'info');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let done = 0;
|
||||
@ -1567,7 +1642,7 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel) {
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || e.error || r.statusText); }
|
||||
data = await r.json();
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') break;
|
||||
if (err.name === 'AbortError') { _audiobook.cancel = true; break; }
|
||||
view.note(`API Error: ${err.message}`);
|
||||
continue;
|
||||
}
|
||||
@ -1582,14 +1657,20 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel) {
|
||||
|
||||
view.addSegments([targetSeg]);
|
||||
}
|
||||
view.update(unknownIdxs.length);
|
||||
view.update(_audiobook.cancel ? done : unknownIdxs.length);
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') view.note('Error recasting unknown: ' + e.message);
|
||||
} finally {
|
||||
_audiobook.running = false;
|
||||
_audiobook.abort = null;
|
||||
}
|
||||
|
||||
if (_audiobook.cancel) { view.done(); toast('Recast cancelled', 'error'); return; }
|
||||
if (_audiobook.cancel) {
|
||||
if (_audiobook.lastText) _abSaveDraft(_audiobook.segments || [], _audiobook.roster || [], _audiobook.lastText, -1, -1);
|
||||
view.done({ stopped: true, message: 'Recast stopped. Existing cast preserved.' });
|
||||
toast('Recast stopped. Existing cast preserved.', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const speakers = new Set(segs.filter(s => s.type === 'dialogue' && s.speaker).map(s => s.speaker));
|
||||
const summary = `${speakers.size} character${speakers.size !== 1 ? 's' : ''} · ${segs.length} segments`;
|
||||
@ -1718,7 +1799,14 @@ async function audiobookCast(overrideUrl, overrideModel) {
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') { _audiobook.running = false; view.done(); return; }
|
||||
if (err.name === 'AbortError') {
|
||||
_audiobook.cancel = true;
|
||||
_audiobook.running = false;
|
||||
_audiobook.abort = null;
|
||||
view.done({ stopped: true, message: 'Casting stopped before any passage completed.' });
|
||||
toast('Casting stopped before any passages were saved.', 'info');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const allSegments = [];
|
||||
@ -1726,6 +1814,7 @@ async function audiobookCast(overrideUrl, overrideModel) {
|
||||
const roster = [];
|
||||
let narrationOnly = 0; // passages with no quotes at all — legitimately all narration
|
||||
let degraded = 0; // passages with dialogue the LLM couldn't analyse → quotes auto-extracted
|
||||
let completedChunks = 0;
|
||||
// Page-mark tracking for visual page-break rows in the casting feed.
|
||||
const _pgMarks = (_audiobook.pageMarks || []).slice();
|
||||
if (_pgMarks.length && _pgMarks[0].offset <= 2) _pgMarks.shift(); // skip page-1 mark at pos 0
|
||||
@ -1744,6 +1833,8 @@ async function audiobookCast(overrideUrl, overrideModel) {
|
||||
if (!audiobookHasDialogue(chunks[i])) {
|
||||
const seg = { speaker: 'Narrator', type: 'narration', text: chunks[i], emotion: '' };
|
||||
allSegments.push(seg); narrationOnly++; view.addSegments([seg]);
|
||||
completedChunks = i + 1;
|
||||
_abSaveDraft(allSegments, roster, text, completedChunks, chunks.length);
|
||||
continue;
|
||||
}
|
||||
// recent attributed dialogue → lets the LLM continue turn-taking across the boundary
|
||||
@ -1872,28 +1963,47 @@ async function audiobookCast(overrideUrl, overrideModel) {
|
||||
segs.forEach(s => { const speakerName = (s.type !== 'dialogue' || !s.speaker || s.speaker.toLowerCase() === 'narrator') ? 'Narrator' : s.speaker; if (!/^Unknown|Unbekannt/i.test(speakerName) && !roster.includes(speakerName)) roster.push(speakerName); });
|
||||
segs.forEach(s => allSegments.push(s));
|
||||
view.addSegments(segs);
|
||||
_abSaveDraft(allSegments, roster, text, i + 1, chunks.length);
|
||||
completedChunks = i + 1;
|
||||
_abSaveDraft(allSegments, roster, text, completedChunks, chunks.length);
|
||||
}
|
||||
view.update(_audiobook.cancel ? completedChunks : chunks.length);
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
_audiobook.cancel = true;
|
||||
view.note('Casting stopped. Completed passages were preserved.');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
view.update(chunks.length);
|
||||
} finally {
|
||||
_audiobook.running = false;
|
||||
_audiobook.abort = null;
|
||||
}
|
||||
|
||||
if (!allSegments.length) { view.done(); toast('No segments produced', 'error'); return; }
|
||||
if (!allSegments.length) {
|
||||
if (_audiobook.cancel) {
|
||||
view.done({ stopped: true, message: 'Casting stopped before any passage completed.' });
|
||||
toast('Casting stopped before any passages were saved.', 'info');
|
||||
return;
|
||||
}
|
||||
view.done();
|
||||
toast('No segments produced', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
_audiobook.segments = allSegments;
|
||||
_audiobook.lastText = text;
|
||||
_audiobook.roster = roster;
|
||||
_audiobook.narratedPassages = narrationOnly;
|
||||
_audiobook.degraded = degraded;
|
||||
_abSaveDraft(allSegments, roster, text, chunks.length, chunks.length); // mark 100% complete
|
||||
_abSaveDraft(allSegments, roster, text, _audiobook.cancel ? completedChunks : chunks.length, chunks.length);
|
||||
audiobookSaveAsRehearsal({ silent: true }); // persist to Rehearser IndexedDB
|
||||
|
||||
if (_audiobook.cancel) toast('Casting stopped early. Progress preserved.', 'info');
|
||||
|
||||
// Park the panel with a "Review & cast" button (don't auto-pop, in case you wandered off)
|
||||
const speakers = new Set(allSegments.filter(s => s.type === 'dialogue' && s.speaker).map(s => s.speaker));
|
||||
const summary = `${speakers.size} character${speakers.size !== 1 ? 's' : ''} · ${allSegments.length} segments`;
|
||||
const stoppedEarly = _audiobook.cancel && completedChunks < chunks.length;
|
||||
const summary = `${stoppedEarly ? 'Stopped early · ' : ''}${speakers.size} character${speakers.size !== 1 ? 's' : ''} · ${allSegments.length} segments`;
|
||||
view.complete(summary, audiobookShowPreview, audiobookCast, audiobookRecastUnknown);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user