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:
parent
e02ca4d703
commit
c15a91d229
11
CHANGELOG.md
11
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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user