From 495550bf6affda79c282622e9babea6b92e56765 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 26 Jun 2026 21:22:40 +0200 Subject: [PATCH] =?UTF-8?q?Release=20v1.9.0=20=E2=80=94=20Character=20Libr?= =?UTF-8?q?ary=20+=20richer=20character=20sheets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a persistent, book-scoped Character Library (new Characters section, IndexedDB) that auto-fills from Character-sheet analysis with editable cards. Enrich extraction with six narrative fields (backstory, relationships, motivation, fears, mannerisms, voice/speech) plus the greyscale Good↔Evil alignment bar, arc arrow, and 5-area Deep Analysis. Add ⋯ separators between non-contiguous passages in Recast unknown, and harden dialogue attribution against hallucination with same-language emotion tags. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 15 ++ VERSION | 2 +- routes/conversation.py | 204 ++++++++++----- scripts/minify.mjs | 1 + static/index.html | 6 +- static/js/audiobook.js | 406 +++++++++++++++++++++++------- static/js/character-sheets.js | 229 ++++++++++++++--- static/js/characters-library.js | 281 +++++++++++++++++++++ static/js/reader.js | 73 +++++- static/js/rehearser.js | 35 ++- static/loader.js | 3 +- static/nav.js | 3 +- static/sections/s-characters.html | 26 ++ static/style.css | 38 ++- 14 files changed, 1102 insertions(+), 220 deletions(-) create mode 100644 static/js/characters-library.js create mode 100644 static/sections/s-characters.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 507a2e6..ac48fbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,21 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.9.0] — 2026-06-26 + +### Added +- **Character Library**: a new **Characters** section in the sidebar collects every character the LLM extracts into a persistent, browsable library (IndexedDB), grouped by the book or script they came from. Running **Character sheets** from Read Aloud or the Script Rehearser now auto-saves each character (keyed by *book + name*), merging in new detail on re-runs. Each card can be **edited** inline or **deleted**, and keeps its greyscale Good↔Evil alignment bar, arc arrow, page/line sources, and the 5-area psychological **Deep Analysis**. New module `static/js/characters-library.js`. +- **Richer character sheets**: extraction now gathers six more book-derived fields per character — *Backstory & Origin, Relationships, Motivation, Fears, Mannerisms & Habits,* and a casting-focused *Voice & Speech* pattern (accent, pacing, register, verbal tics). +- **Character sheets — morality at a glance**: each sheet now shows a **greyscale Good↔Evil alignment bar** (white = good, black = evil) with a 0–100 score, plus an **arc arrow** indicating whether the character stays put, *descends* (↘ good→bad), *redeems* (↗ bad→good), or follows a *complex* (↕) path. Also added **Clothing & Appearance** and **Capabilities** fields and richer physical detail (height, hair, eyes, skin, gait), with sources now carrying a short category **line hint**. +- **Character sheets — Deep Analysis**: a per-character button runs a 5-area psychological & narrative study (Core Flaw & Desire · Agency & Passivity · Dialogue & Voice · Narrative Arc · Paradox & Depth) in a modal. New endpoint `POST /api/character-deep-analysis`. +- **Audiobook casting — Recast & rescue tools**: after a cast run you can now **Recast all** (re-run the whole document with tweaked settings/prompt), **Recast unknown** (re-attribute only the leftover *Unknown* lines using surrounding context), run a **2nd Quality Run — Verify** pass that re-checks every speaker assignment and resolves Unknowns, and **Save script** straight to Script Rehearsals without leaving the page. + +### Changed +- **Recast unknown — passage separators**: re-analysing only the *Unknown* speakers now shows a `⋯` divider between non-adjacent passages, so it's clear where one excerpt ends and another begins. +- **Dialogue attribution — fidelity & language**: the attribution prompt now forbids hallucinating/summarising (segments must reconstruct the passage word-for-word), emits emotion tags in the **same language** as the text (e.g. German *wütend/flüsternd*), and demands strict JSON. Removed the brittle text-script fallback parser that could mis-split prompt echoes into fake speakers. + +--- + ## [1.8.1] — 2026-06-25 ### Added diff --git a/VERSION b/VERSION index a8fdfda..f8e233b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.1 +1.9.0 diff --git a/routes/conversation.py b/routes/conversation.py index 5f50402..59d6ad8 100644 --- a/routes/conversation.py +++ b/routes/conversation.py @@ -466,9 +466,9 @@ async def character_sheets(request: Request): lang_hint = f" The text language is {language}; write the sheet in that language." if language else "" system = ( - "You are an expert dramaturge and tabletop RPG game master building character sheets that FILL UP " - "as a book is read passage by passage. Read this passage and extract playable, action-oriented " - "character sheets — sheets an actor can use to immediately know how to PLAY the character.\n" + "You are an expert dramaturge, developmental editor, and tabletop RPG game master building rich " + "character sheets passage by passage as a book is read. Extract playable, action-oriented sheets " + "an actor can use to immediately know how to PLAY the character.\n" "PROGRESSIVE FILLING: you may be given the sheets built so far. For returning characters, ADD any " "NEW detail this passage reveals and refine vague fields; do not contradict solid earlier facts or " "blank out a field you cannot improve. Add brand-new characters as they appear. Leave a field empty " @@ -478,22 +478,37 @@ async def character_sheets(request: Request): "For each character output these fields:\n" "- name, aliases\n" "- archetype: a two-word role summary (e.g. 'Ruthless Scholar')\n" - "- physical: age, build, vocal quality, posture. Use ONLY the metric system for any height/weight.\n" + "- physical: age, height, build, hair, eyes, skin, posture, gait, vocal quality. Use ONLY metric system.\n" + "- clothing: distinctive clothing, armour, accessories — as observed in the text\n" "- alignment: strict moral code + the one line they will never cross\n" + "- moral_alignment_score: integer 0–100. 100 = purely good/heroic, 0 = purely evil/villainous, 50 = neutral/ambiguous\n" + "- arc_direction: one of: 'stable-good', 'stable-bad', 'neutral', 'good-to-bad', 'bad-to-good', 'complex'\n" + "- arc_note: one sentence explaining the arc or moral position visible so far\n" "- attribute_high / attribute_low: highest and lowest natural attribute (Charisma, Intelligence, Wisdom, Agility…)\n" "- skills: what they are demonstrably good at in the story\n" + "- capabilities: combat, magic, social, technical, or other demonstrated abilities\n" + "- backstory: origin, formative background and history revealed in the text\n" + "- relationships: key allies, family, rivals and enemies — name them and how they relate\n" + "- motivation: the inner drive — WHY they pursue what they pursue (distinct from the win condition)\n" + "- fears: their deepest fears, phobias or dread\n" + "- mannerisms: habitual gestures, tics, body language, habits and quirks\n" + "- voice_pattern: speech style — accent, pacing, vocabulary, register and verbal tics (for voice casting)\n" "- inventory: 1-3 defining items/props/clothing (array of short strings)\n" "- secret: dark secret or fatal flaw\n" "- conflict_style: fight, flight, or manipulate — how they act when cornered\n" "- win_condition: the specific event that would make them feel they have won\n" "- tier: 'main' or 'supporting'\n" - "- sources: array of {page, quote} — the page number from the nearest [p.N] marker and a short " - "verbatim quote that supports the sheet (1-3 entries). Use null page if unknown.\n" + "- sources: array of {page, quote, line_hint} — the page number from the nearest [p.N] marker, " + "a short verbatim quote that supports the sheet (1-5 entries), and a brief label (e.g. 'appearance', 'alignment'). " + "Use null page if unknown.\n" "Reuse the EXACT names from the known-characters list for returning characters. " "Respond with STRICT JSON only:\n" - '{"sheets":[{"name":"","aliases":"","archetype":"","physical":"","alignment":"",' - '"attribute_high":"","attribute_low":"","skills":"","inventory":[],"secret":"",' - '"conflict_style":"","win_condition":"","tier":"main","sources":[{"page":1,"quote":""}]}]}\n/no-think' + '{"sheets":[{"name":"","aliases":"","archetype":"","physical":"","clothing":"",' + '"alignment":"","moral_alignment_score":50,"arc_direction":"neutral","arc_note":"",' + '"attribute_high":"","attribute_low":"","skills":"","capabilities":"",' + '"backstory":"","relationships":"","motivation":"","fears":"","mannerisms":"","voice_pattern":"",' + '"inventory":[],"secret":"","conflict_style":"","win_condition":"",' + '"tier":"main","sources":[{"page":1,"quote":"","line_hint":""}]}]}\n/no-think' ) user = ( ("Known characters so far: " + ", ".join(str(n) for n in known) + "\n\n" if known else "") @@ -506,7 +521,7 @@ async def character_sheets(request: Request): {"role": "user", "content": user}, ], "temperature": 0.4, - "max_tokens": 3500, + "max_tokens": 4096, } if model: payload["model"] = model @@ -546,14 +561,110 @@ async def character_sheets(request: Request): elif not isinstance(inv, list): inv = [] src = s.get("sources") if isinstance(s.get("sources"), list) else [] - s.update({"name": name, "inventory": inv[:3], - "tier": "main" if str(s.get("tier") or "").lower().startswith("main") else "supporting", - "sources": src[:3]}) + # Clamp moral alignment score + try: + mas = int(s.get("moral_alignment_score") or 50) + mas = max(0, min(100, mas)) + except (TypeError, ValueError): + mas = 50 + arc = str(s.get("arc_direction") or "neutral").strip() + if arc not in ("stable-good", "stable-bad", "neutral", "good-to-bad", "bad-to-good", "complex"): + arc = "neutral" + s.update({ + "name": name, "inventory": inv[:3], + "tier": "main" if str(s.get("tier") or "").lower().startswith("main") else "supporting", + "sources": src[:5], + "moral_alignment_score": mas, + "arc_direction": arc, + }) clean.append(s) names.append(name) return {"sheets": clean, "characters": names} +@router.post("/api/character-deep-analysis") +async def character_deep_analysis(request: Request): + """Run a deep 5-area psychological analysis of a single character. + + Body: {name, role, goal, summary, text_excerpt, language, llm_url, model} + Returns: {analysis: {core_flaw, agency, dialogue_voice, narrative_arc, paradox}} + """ + data = await request.json() + name: str = (data.get("name") or "").strip() + role: str = (data.get("role") or "unknown").strip() + goal: str = (data.get("goal") or "").strip() + summary: str = (data.get("summary") or "").strip() + excerpt: str = (data.get("text_excerpt") or "").strip() + language: str = (data.get("language") or "").strip() + _settings = _load_settings() + llm_url: str = (data.get("llm_url") or _settings.get("llm_url") or "http://localhost:11434/v1").rstrip("/") + model: str = (data.get("model") or _settings.get("llm_model") or "").strip() + + if not name: + raise HTTPException(400, "Character name required") + + lang_note = f" Write the analysis in {language}." if language else "" + + system = ( + f"You are an expert developmental editor, literary coach, and psychologist specializing in " + f"profound character studies.{lang_note} Provide a deep, multi-layered psychological and " + f"narrative analysis of the character in exactly this JSON structure:\n" + '{"core_flaw":"",' + '"agency":"",' + '"dialogue_voice":"