Add Casting back-navigation, fix truncated prompt generation (v1.12.91)

Characters/Cast gains a "Casting" button back to the active casting
session, the pipeline stepper renders on the Library section, and the
stepper's Cast Characters stop navigates instead of side-effect-running
sheet generation. Prompt generation: 4096-token budget (1600 truncated
the four-prompt JSON so two fields silently arrived empty), truncated
answers salvage completed fields, all-empty responses fail loudly, and
partial results name the missing prompts. The PDF-extraction progress
pill is enlarged and vertically centered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-07-04 17:43:51 +02:00
parent e02ca4d703
commit c15a91d229
10 changed files with 63 additions and 13 deletions

View File

@ -9,6 +9,17 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
## [1.12.91] — 2026-07-04
### Fixed
- **No way back from Cast Characters to the casting script** — the Characters/Cast view's production bar now has a "Casting" button that returns to the active casting session (via the reader's cast-restore), the pipeline stepper now also renders on the Library section so prev/next navigation works from there too, and the stepper's "Cast Characters" stop now *navigates* to the Characters/Cast view instead of kicking off sheet generation as a side effect of clicking a navigation element.
- **Two of the four generation prompts silently came back empty** — the LLM's answer was truncated by a too-small output budget (1600 tokens for four prompts, one of them multi-section), the JSON parse failed, and the UI showed success with two empty boxes. The budget is now 4096 tokens, a cut-off answer is salvaged for the fields that did complete, an all-empty response is a loud server error instead of a silent success, and a partial result names exactly which prompts are missing so you know to hit Generate again.
### Changed
- **"Reading PDF… page N / M" progress pill enlarged and vertically centered** in the document pane (was a small pill pinned to the top edge), so long extractions have a clearly visible "still working" signal.
---
## [1.12.90] — 2026-07-04
### Fixed

View File

@ -1 +1 @@
1.12.90
1.12.91

View File

@ -885,7 +885,10 @@ async def character_generate_prompts(request: Request):
{"role": "user", "content": user},
],
"temperature": 0.7,
"max_tokens": 1600,
# Four prompts, one of them multi-section (SillyTavern) — a tighter cap
# truncated the JSON mid-output, the parse failed, and the two later
# fields silently came back empty.
"max_tokens": 4096,
}
if model:
payload["model"] = model
@ -901,8 +904,14 @@ async def character_generate_prompts(request: Request):
raise HTTPException(502, f"LLM prompt generation failed: {e}")
content = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip() or raw
block = _extract_json_block(content)
# If the model still got cut off mid-string, closing the dangling string +
# object often salvages the fields that DID complete.
candidates = [content, block]
if block:
candidates.extend([block + "\"}", block + "}"])
result = {}
for cand in (content, _extract_json_block(content)):
for cand in candidates:
if not cand:
continue
try:
@ -912,12 +921,17 @@ async def character_generate_prompts(request: Request):
break
except Exception:
continue
return {
out = {
"voice_design_prompt": str(result.get("voice_design_prompt") or "").strip(),
"image_prompt": str(result.get("image_prompt") or "").strip(),
"silly_tavern_prompt": str(result.get("silly_tavern_prompt") or "").strip(),
"concept_art_prompt": str(result.get("concept_art_prompt") or "").strip(),
}
if not any(out.values()):
# A silent all-empty response looked identical to success in the UI —
# fail loudly so the client can show a real error instead.
raise HTTPException(502, "LLM returned no parseable prompts — try again (or a different model)")
return out
@router.post("/api/attribute-dialogue")

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.12.90">
<meta name="app-version" content="1.12.91">
<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.90">
<link rel="stylesheet" href="/static/style.css?v=1.12.91">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.12.90"></script>
<script src="/static/loader.js?v=1.12.91"></script>
</body>
</html>

View File

@ -62,6 +62,7 @@ async function libraryRenderCharacters() {
prod.innerHTML = '<div class="lib-chars-prod-head" style="--pk1:' + cov.c1 + ';--pk2:' + cov.c2 + '">'
+ '<div class="lib-chars-prod-title"><span class="mdi mdi-bookshelf"></span> ' + escHtml(book) + '</div>'
+ '<div class="lib-chars-prod-actions">'
+ '<button class="btn-secondary btn-sm lib-chars-casting-btn" data-book="' + escHtml(book) + '" title="Back to the casting script (speaker attribution)"><span class="mdi mdi-drama-masks"></span> Casting</button>'
+ '<button class="btn-secondary btn-sm lib-chars-cast-btn" data-book="' + escHtml(book) + '" title="Re-run character sheet generation"><span class="mdi mdi-account-details-outline"></span> Cast Characters</button>'
+ '<button class="btn-secondary btn-sm lib-chars-reh-btn" data-book="' + escHtml(book) + '" title="Open in Script Rehearsal"><span class="mdi mdi-theater"></span> Rehearse</button>'
+ '<button class="btn-secondary btn-sm lib-chars-read-btn" data-book="' + escHtml(book) + '" title="Open in Read Aloud"><span class="mdi mdi-book-open-page-variant-outline"></span> Read Aloud</button>'
@ -72,6 +73,12 @@ async function libraryRenderCharacters() {
+ '</div>';
// Action buttons
prod.querySelector('.lib-chars-casting-btn').addEventListener('click', function () {
// Back to the casting script. If a casting session is active, navTo's
// synchronous cast-restore (nav.js) reopens it directly; otherwise this
// lands on the reader so the book can be opened and cast from there.
if (typeof navTo === 'function') navTo('s-reader');
});
prod.querySelector('.lib-chars-cast-btn').addEventListener('click', function () {
if (typeof productionOpenInReader === 'function') productionOpenInReader(book);
toast('Open the book in Read Aloud then click Cast Characters', 'info');
@ -607,12 +614,18 @@ async function _charDetailPage(rec, allChars) {
if (!r.ok) throw new Error((await r.json().catch(function () { return {}; })).detail || r.statusText);
const d = await r.json();
if (!rec.sheet) rec.sheet = {};
['voice_design_prompt', 'image_prompt', 'silly_tavern_prompt', 'concept_art_prompt'].forEach(function (k) {
const labels = { voice_design_prompt: 'Voice Design', image_prompt: 'Character Image', silly_tavern_prompt: 'SillyTavern', concept_art_prompt: 'Concept Art' };
const missing = [];
Object.keys(labels).forEach(function (k) {
if (d[k]) rec.sheet[k] = d[k];
else if (!rec.sheet[k]) missing.push(labels[k]);
});
rec.updated = new Date();
if (typeof clPut === 'function') await clPut(rec);
toast('Generation prompts created', 'success');
// A partially-cut-off LLM answer used to look exactly like success with
// two silently empty boxes — say which ones are missing instead.
if (missing.length) toast('Generated, but incomplete — missing: ' + missing.join(', ') + '. Click Generate again.', 'error');
else toast('Generation prompts created', 'success');
_charDetailPage(rec, allChars); // re-render so the boxes show the new content
} catch (err) {
toast('Prompt generation failed: ' + (err.message || err), 'error');

View File

@ -26,6 +26,9 @@ window.navLibraryView = function (view) {
document.querySelectorAll('[data-library-panel]').forEach(function (el) {
el.classList.toggle('is-active', el.dataset.libraryPanel === view);
});
// Characters/Cast is the pipeline's "Cast Characters" stop — keep the
// stepper's current-step highlight in sync when the tab is opened directly.
if (view === 'characters' && typeof refreshWorkflowCrumbs === 'function') refreshWorkflowCrumbs('chars');
libraryRender(view);
};

View File

@ -601,13 +601,15 @@ async function readerLoadPdf(file) {
await readerExtractPdfText(loaded);
}
// Lightweight floating parse-progress line shown in the document pane for big PDFs.
// Floating parse-progress pill for big PDFs — sticky at ~mid-height of the
// document pane and sized to be legible from across the room, since a 300-page
// extraction can take a while and this is the only "still working" signal.
function readerShowParseProgress(total) {
const doc = $('reader-doc'); if (!doc) return { update() {}, done() {} };
const el = document.createElement('div');
el.className = 'reader-parsing';
el.innerHTML = '<span class="mdi mdi-loading mdi-spin" style="font-size:16px;"></span> <span>Reading PDF… page 0 / ' + total + '</span>';
el.style.cssText = 'position:sticky; top:12px; z-index:100; margin:0 auto -42px; background:var(--accent); color:#fff; padding:6px 14px; border-radius:20px; font-size:12px; display:inline-flex; align-items:center; gap:8px; box-shadow:0 4px 12px rgba(0,0,0,.25); font-weight:600;';
el.innerHTML = '<span class="mdi mdi-loading mdi-spin" style="font-size:26px;"></span> <span>Reading PDF… page 0 / ' + total + '</span>';
el.style.cssText = 'position:sticky; top:40vh; z-index:100; margin:0 auto -64px; background:var(--accent); color:#fff; padding:14px 28px; border-radius:32px; font-size:18px; display:inline-flex; align-items:center; gap:12px; box-shadow:0 6px 24px rgba(0,0,0,.35); font-weight:700;';
doc.insertBefore(el, doc.firstChild);
const label = el.querySelector('span:last-child');
return {

View File

@ -818,7 +818,11 @@ function workflowCrumbGo(key) {
if (typeof navTo === 'function') navTo('s-reader');
if (typeof audiobookOpenCastView === 'function') audiobookOpenCastView();
} else if (key === 'chars') {
if (typeof csForReader === 'function') csForReader();
// Navigate to the Characters/Cast library view — generating sheets stays
// on the explicit "Cast Characters" buttons; a navigation stop must never
// kick off an LLM run as a side effect.
if (typeof navTo === 'function') navTo('s-library');
if (typeof navLibraryView === 'function') navLibraryView('characters');
_wfActive = 'chars';
refreshWorkflowCrumbs();
} else if (key === 'rehearser') {

View File

@ -204,6 +204,7 @@
if (typeof window.refreshWorkflowCrumbs === 'function') {
if (sectionId === 's-rehearser') window.refreshWorkflowCrumbs('rehearser');
else if (sectionId === 's-reader') window.refreshWorkflowCrumbs('source');
else if (sectionId === 's-library' && (window._libraryView || 'books') === 'characters') window.refreshWorkflowCrumbs('chars');
}
};

View File

@ -6,6 +6,8 @@
</div>
</div>
<div class="wf-stepper" id="wf-stepper-library" hidden></div>
<div class="lib-tabs">
<button class="lib-tab is-active" data-library-view="books" onclick="navLibraryView('books')"><span class="mdi mdi-book-open-page-variant-outline"></span> Books</button>
<button class="lib-tab" data-library-view="plays" onclick="navLibraryView('plays')"><span class="mdi mdi-theater"></span> Theater Plays</button>