From c15a91d229b4a066224593bbcae09f19ff4acdc2 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Sat, 4 Jul 2026 17:43:51 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 11 +++++++++++ VERSION | 2 +- routes/conversation.py | 20 +++++++++++++++++--- static/index.html | 6 +++--- static/js/library-characters.js | 17 +++++++++++++++-- static/js/library.js | 3 +++ static/js/reader.js | 8 +++++--- static/js/utils.js | 6 +++++- static/nav.js | 1 + static/sections/s-library.html | 2 ++ 10 files changed, 63 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bfdbb5..c981406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/VERSION b/VERSION index 8849819..5771ee1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.90 +1.12.91 diff --git a/routes/conversation.py b/routes/conversation.py index 780eaeb..dff3627 100644 --- a/routes/conversation.py +++ b/routes/conversation.py @@ -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".*?", "", 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") diff --git a/static/index.html b/static/index.html index 23fed76..91af6c8 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/library-characters.js b/static/js/library-characters.js index fdfd248..959d866 100644 --- a/static/js/library-characters.js +++ b/static/js/library-characters.js @@ -62,6 +62,7 @@ async function libraryRenderCharacters() { prod.innerHTML = '
' + '
' + escHtml(book) + '
' + '
' + + '' + '' + '' + '' @@ -72,6 +73,12 @@ async function libraryRenderCharacters() { + '
'; // 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'); diff --git a/static/js/library.js b/static/js/library.js index e258f8a..c69a339 100644 --- a/static/js/library.js +++ b/static/js/library.js @@ -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); }; diff --git a/static/js/reader.js b/static/js/reader.js index 760504a..f24df0a 100644 --- a/static/js/reader.js +++ b/static/js/reader.js @@ -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 = ' Reading PDF… page 0 / ' + total + ''; - 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 = ' Reading PDF… page 0 / ' + total + ''; + 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 { diff --git a/static/js/utils.js b/static/js/utils.js index 64a6d1d..427e0ff 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -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') { diff --git a/static/nav.js b/static/nav.js index c27e522..dd1603b 100644 --- a/static/nav.js +++ b/static/nav.js @@ -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'); } }; diff --git a/static/sections/s-library.html b/static/sections/s-library.html index f9633b1..e4fafb6 100644 --- a/static/sections/s-library.html +++ b/static/sections/s-library.html @@ -6,6 +6,8 @@
+ +