Scope Recast-selected to only the relevant passages (v1.13.9)
Recasting one character out of a 149-passage book was reading the entire text every time, exactly as flagged: "you only need to read a couple of paragraphs before and after his name." csForReaderSelective now matches the picked character's name + known aliases against the book's chunks, keeps one chunk of context on either side for pronoun/"he" resolution, and only extracts from those - falling back to the full book only if nothing matched at all (e.g. a name typo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1b38fce792
commit
6a9c827ec9
@ -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
|
## [1.13.8] — 2026-07-06
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@ -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.13.8">
|
<meta name="app-version" content="1.13.9">
|
||||||
<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.13.8">
|
<link rel="stylesheet" href="/static/style.css?v=1.13.9">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -365,7 +365,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.13.8"></script>
|
<script src="/static/loader.js?v=1.13.9"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1070,15 +1070,15 @@ function _abOpenRecastCharsMenu(anchorEl, bookTitle, existingChars) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkbox picker for "recast selected" — re-scans the whole book (extraction
|
// Checkbox picker for "recast selected" — csForReaderSelective narrows
|
||||||
// is passage-by-passage, so any page could mention any character) but only
|
// extraction down to just the passages mentioning the picked character(s)
|
||||||
// saves updated sheets for the characters checked here.
|
// (plus neighbouring context) instead of the whole book.
|
||||||
function _abOpenRecastSelectPopup(bookTitle, existingChars) {
|
function _abOpenRecastSelectPopup(bookTitle, existingChars) {
|
||||||
const ov = document.createElement('div');
|
const ov = document.createElement('div');
|
||||||
ov.className = 'audiobook-overlay';
|
ov.className = 'audiobook-overlay';
|
||||||
ov.innerHTML = '<div class="audiobook-box">'
|
ov.innerHTML = '<div class="audiobook-box">'
|
||||||
+ '<div class="audiobook-title"><span class="mdi mdi-account-details-outline"></span> Recast selected characters</div>'
|
+ '<div class="audiobook-title"><span class="mdi mdi-account-details-outline"></span> Recast selected characters</div>'
|
||||||
+ '<div class="audiobook-msg">Re-scans the whole book, but only refreshes the characters you pick below — everyone else\'s sheet is left as-is.</div>'
|
+ '<div class="audiobook-msg">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.</div>'
|
||||||
+ '<div class="ab-recast-select-list">'
|
+ '<div class="ab-recast-select-list">'
|
||||||
+ existingChars.map(c => '<label><input type="checkbox" class="ab-recast-cb" value="' + escHtml(c.name) + '"> ' + escHtml(c.name) + '</label>').join('')
|
+ existingChars.map(c => '<label><input type="checkbox" class="ab-recast-cb" value="' + escHtml(c.name) + '"> ' + escHtml(c.name) + '</label>').join('')
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
|
|||||||
@ -788,12 +788,37 @@ async function csForReaderSelective(selectedNames) {
|
|||||||
if (!wanted.size) { toast('No characters selected', 'error'); return; }
|
if (!wanted.size) { toast('No characters selected', 'error'); return; }
|
||||||
const text = csReaderText();
|
const text = csReaderText();
|
||||||
const book = readerState.title || 'Untitled book';
|
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
|
// Only the picked names go in as the target roster — passing the FULL
|
||||||
// roster here (like csForReader does) made the progress dialog show every
|
// roster here (like csForReader does) made the progress dialog show every
|
||||||
// character as "queued" even though just one or two were selected, which
|
// 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
|
// read as "it's recasting everyone" even though the save step at the end
|
||||||
// was already correctly filtered down to the picks.
|
// 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;
|
if (!sheets) return;
|
||||||
const filtered = sheets.filter(s => wanted.has(String(s.name || '').trim().toLowerCase()));
|
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; }
|
if (!filtered.length) { toast('None of the selected characters turned up in this pass — try again or pick different ones', 'error'); return; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user