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) {
-
+