diff --git a/CHANGELOG.md b/CHANGELOG.md index 736b205..7ca08fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/VERSION b/VERSION index 85badc6..a389f8c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.56 +1.12.57 diff --git a/static/index.html b/static/index.html index dabd3a4..72b49fd 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -362,7 +362,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/audiobook.js b/static/js/audiobook.js index 2cd2966..94e3406 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -418,10 +418,26 @@ STRIKTE FORMAT- UND TEXTREGELN: } }; + const setStoppingState = () => { + const cancelBtn = panel.querySelector('#ab-cv-cancel'); + if (cancelBtn) { + cancelBtn.disabled = true; + cancelBtn.innerHTML = ' 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 = 'No completed characters yet.'; + } + + const foot = panel.querySelector('#ab-cv-foot'); + const hasSegments = Array.isArray(_audiobook.segments) && _audiobook.segments.length > 0; + foot.hidden = false; + foot.innerHTML = ` ${escHtml(message)}${hasSegments ? '' : ''}`; + + 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); }