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
|
## [1.12.90] — 2026-07-04
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@ -885,7 +885,10 @@ async def character_generate_prompts(request: Request):
|
|||||||
{"role": "user", "content": user},
|
{"role": "user", "content": user},
|
||||||
],
|
],
|
||||||
"temperature": 0.7,
|
"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:
|
if model:
|
||||||
payload["model"] = model
|
payload["model"] = model
|
||||||
@ -901,8 +904,14 @@ async def character_generate_prompts(request: Request):
|
|||||||
raise HTTPException(502, f"LLM prompt generation failed: {e}")
|
raise HTTPException(502, f"LLM prompt generation failed: {e}")
|
||||||
|
|
||||||
content = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip() or raw
|
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 = {}
|
result = {}
|
||||||
for cand in (content, _extract_json_block(content)):
|
for cand in candidates:
|
||||||
if not cand:
|
if not cand:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
@ -912,12 +921,17 @@ async def character_generate_prompts(request: Request):
|
|||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
return {
|
out = {
|
||||||
"voice_design_prompt": str(result.get("voice_design_prompt") or "").strip(),
|
"voice_design_prompt": str(result.get("voice_design_prompt") or "").strip(),
|
||||||
"image_prompt": str(result.get("image_prompt") or "").strip(),
|
"image_prompt": str(result.get("image_prompt") or "").strip(),
|
||||||
"silly_tavern_prompt": str(result.get("silly_tavern_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(),
|
"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")
|
@router.post("/api/attribute-dialogue")
|
||||||
|
|||||||
@ -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.12.90">
|
<meta name="app-version" content="1.12.91">
|
||||||
<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.12.90">
|
<link rel="stylesheet" href="/static/style.css?v=1.12.91">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── 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.12.90"></script>
|
<script src="/static/loader.js?v=1.12.91"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -62,6 +62,7 @@ async function libraryRenderCharacters() {
|
|||||||
prod.innerHTML = '<div class="lib-chars-prod-head" style="--pk1:' + cov.c1 + ';--pk2:' + cov.c2 + '">'
|
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-title"><span class="mdi mdi-bookshelf"></span> ' + escHtml(book) + '</div>'
|
||||||
+ '<div class="lib-chars-prod-actions">'
|
+ '<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-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-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>'
|
+ '<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>';
|
+ '</div>';
|
||||||
|
|
||||||
// Action buttons
|
// 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 () {
|
prod.querySelector('.lib-chars-cast-btn').addEventListener('click', function () {
|
||||||
if (typeof productionOpenInReader === 'function') productionOpenInReader(book);
|
if (typeof productionOpenInReader === 'function') productionOpenInReader(book);
|
||||||
toast('Open the book in Read Aloud then click Cast Characters', 'info');
|
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);
|
if (!r.ok) throw new Error((await r.json().catch(function () { return {}; })).detail || r.statusText);
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (!rec.sheet) rec.sheet = {};
|
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];
|
if (d[k]) rec.sheet[k] = d[k];
|
||||||
|
else if (!rec.sheet[k]) missing.push(labels[k]);
|
||||||
});
|
});
|
||||||
rec.updated = new Date();
|
rec.updated = new Date();
|
||||||
if (typeof clPut === 'function') await clPut(rec);
|
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
|
_charDetailPage(rec, allChars); // re-render so the boxes show the new content
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast('Prompt generation failed: ' + (err.message || err), 'error');
|
toast('Prompt generation failed: ' + (err.message || err), 'error');
|
||||||
|
|||||||
@ -26,6 +26,9 @@ window.navLibraryView = function (view) {
|
|||||||
document.querySelectorAll('[data-library-panel]').forEach(function (el) {
|
document.querySelectorAll('[data-library-panel]').forEach(function (el) {
|
||||||
el.classList.toggle('is-active', el.dataset.libraryPanel === view);
|
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);
|
libraryRender(view);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -601,13 +601,15 @@ async function readerLoadPdf(file) {
|
|||||||
await readerExtractPdfText(loaded);
|
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) {
|
function readerShowParseProgress(total) {
|
||||||
const doc = $('reader-doc'); if (!doc) return { update() {}, done() {} };
|
const doc = $('reader-doc'); if (!doc) return { update() {}, done() {} };
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'reader-parsing';
|
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.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: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.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);
|
doc.insertBefore(el, doc.firstChild);
|
||||||
const label = el.querySelector('span:last-child');
|
const label = el.querySelector('span:last-child');
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -818,7 +818,11 @@ function workflowCrumbGo(key) {
|
|||||||
if (typeof navTo === 'function') navTo('s-reader');
|
if (typeof navTo === 'function') navTo('s-reader');
|
||||||
if (typeof audiobookOpenCastView === 'function') audiobookOpenCastView();
|
if (typeof audiobookOpenCastView === 'function') audiobookOpenCastView();
|
||||||
} else if (key === 'chars') {
|
} 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';
|
_wfActive = 'chars';
|
||||||
refreshWorkflowCrumbs();
|
refreshWorkflowCrumbs();
|
||||||
} else if (key === 'rehearser') {
|
} else if (key === 'rehearser') {
|
||||||
|
|||||||
@ -204,6 +204,7 @@
|
|||||||
if (typeof window.refreshWorkflowCrumbs === 'function') {
|
if (typeof window.refreshWorkflowCrumbs === 'function') {
|
||||||
if (sectionId === 's-rehearser') window.refreshWorkflowCrumbs('rehearser');
|
if (sectionId === 's-rehearser') window.refreshWorkflowCrumbs('rehearser');
|
||||||
else if (sectionId === 's-reader') window.refreshWorkflowCrumbs('source');
|
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>
|
</div>
|
||||||
|
|
||||||
|
<div class="wf-stepper" id="wf-stepper-library" hidden></div>
|
||||||
|
|
||||||
<div class="lib-tabs">
|
<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 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>
|
<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