diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b71b51..12f6100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.13.9] — 2026-07-06 + +### Changed +- **"Recast selected" no longer re-reads the whole book** — it now only sends the passages that actually mention the picked character(s) (matched against their name and known aliases), plus one chunk of surrounding context on either side for pronoun resolution, falling back to the full book only if nothing matched at all. Recasting one character out of dozens now takes a handful of passages instead of the whole thing. + +--- + ## [1.13.8] — 2026-07-06 ### Changed diff --git a/VERSION b/VERSION index 237a256..084f226 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.13.8 +1.13.9 diff --git a/static/index.html b/static/index.html index 75fadd4..d443480 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/audiobook.js b/static/js/audiobook.js index 9afed87..36102c4 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -1070,15 +1070,15 @@ function _abOpenRecastCharsMenu(anchorEl, bookTitle, existingChars) { }); } -// Checkbox picker for "recast selected" — re-scans the whole book (extraction -// is passage-by-passage, so any page could mention any character) but only -// saves updated sheets for the characters checked here. +// Checkbox picker for "recast selected" — csForReaderSelective narrows +// extraction down to just the passages mentioning the picked character(s) +// (plus neighbouring context) instead of the whole book. function _abOpenRecastSelectPopup(bookTitle, existingChars) { const ov = document.createElement('div'); ov.className = 'audiobook-overlay'; ov.innerHTML = '
' + '
Recast selected characters
' - + '
Re-scans the whole book, but only refreshes the characters you pick below — everyone else\'s sheet is left as-is.
' + + '
Only re-reads the passages that mention the characters you pick below (plus a little surrounding context) — everyone else\'s sheet is left as-is.
' + '
' + existingChars.map(c => '').join('') + '
' diff --git a/static/js/character-sheets.js b/static/js/character-sheets.js index c6c1824..e96a1af 100644 --- a/static/js/character-sheets.js +++ b/static/js/character-sheets.js @@ -788,12 +788,37 @@ async function csForReaderSelective(selectedNames) { if (!wanted.size) { toast('No characters selected', 'error'); return; } const text = csReaderText(); const book = readerState.title || 'Untitled book'; + // A targeted recast only needs the passages that actually mention the + // picked character(s) — reading the whole book to update one person was + // pure waste. Match name + known aliases, keep one chunk of context on + // either side of each hit (for pronoun/"he/she" resolution), and only fall + // back to the full text if nothing matched at all (e.g. a name typo). + let scanText = text; + try { + const records = (typeof clGetAllByTagOrBook === 'function') ? await clGetAllByTagOrBook(book) : []; + const needles = []; + for (const rec of records || []) { + if (!wanted.has(String(rec.name || '').trim().toLowerCase())) continue; + needles.push(rec.name); + String(rec.sheet?.aliases || '').split(',').forEach(a => { if (a.trim()) needles.push(a.trim()); }); + } + selectedNames.forEach(n => { if (!needles.some(x => x.toLowerCase() === String(n).toLowerCase())) needles.push(n); }); + const pattern = needles.filter(Boolean).map(n => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + if (pattern.length) { + const re = new RegExp('\\b(?:' + pattern.join('|') + ')\\b', 'i'); + const chunks = splitTextIntoChunks(text, CS_CHUNK_CHARS); + const matched = new Set(); + chunks.forEach((c, i) => { if (re.test(c)) { matched.add(i - 1); matched.add(i); matched.add(i + 1); } }); + const idxs = [...matched].filter(i => i >= 0 && i < chunks.length).sort((a, b) => a - b); + if (idxs.length) scanText = idxs.map(i => chunks[i]).join('\n\n'); + } + } catch (_) { /* fall through to full-text scan */ } // Only the picked names go in as the target roster — passing the FULL // roster here (like csForReader does) made the progress dialog show every // character as "queued" even though just one or two were selected, which // read as "it's recasting everyone" even though the save step at the end // was already correctly filtered down to the picks. - const sheets = await csGenerate(text, null, selectedNames); + const sheets = await csGenerate(scanText, null, selectedNames); if (!sheets) return; const filtered = sheets.filter(s => wanted.has(String(s.name || '').trim().toLowerCase())); if (!filtered.length) { toast('None of the selected characters turned up in this pass — try again or pick different ones', 'error'); return; }