diff --git a/CHANGELOG.md b/CHANGELOG.md
index a11e9e4..c14fda4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,24 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
+## [1.14.21] — 2026-07-09
+
+### Added
+- **A one-character refine action in the audiobook profile panel** so mixed profiles can be re-run on a single selected character instead of recasting the whole book.
+
+### Changed
+- **Character Sheets and audiobook profiles now expose the same identity fields** — first name, last name, full name, aliases, gender, title, and occupation/profession are shown explicitly so one unified sheet layout is easier to read and edit.
+- **The live Passage / Live output box now has a visible drag handle even when collapsed** so the user can tell it is resizable and adjust how much of the streaming output stays visible.
+
+## [1.14.20] — 2026-07-09
+
+### Changed
+- **Character Sheets now puts prompt generation at the bottom of each sheet** with collapsible ready-to-copy boxes for Voice Design, Character Image, SillyTavern Character, and Character Concept Art prompts.
+- **Character sheet evidence is easier to audit** — source hooks now show the supporting quote snippets, and alias labels are written as “also known as” in the UI.
+
+### Fixed
+- **The cast-character flow no longer invents new profiles while refining an already cast roster** — selective recasts now focus on the existing cast and use nearby paragraph windows around each hit as context.
+
## [1.14.19] — 2026-07-09
### Changed
@@ -1639,7 +1657,9 @@ Initial feature-complete release.
---
-[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.19...HEAD
+[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.21...HEAD
+[1.14.21]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.20...v1.14.21
+[1.14.20]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.19...v1.14.20
[1.14.19]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.18...v1.14.19
[1.8.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.8.0...v1.8.1
[1.8.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.7.0...v1.8.0
diff --git a/VERSION b/VERSION
index 6620f60..dad6733 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.14.19
+1.14.21
diff --git a/routes/conversation.py b/routes/conversation.py
index 75b0dfb..769944b 100644
--- a/routes/conversation.py
+++ b/routes/conversation.py
@@ -614,7 +614,7 @@ def _charsheets_prepare(data: dict) -> dict:
"Do not output a blank sheet just because a target is listed.\n"
"- If the passage proves that a target has another name/title/nickname, keep ONE profile using the best known target name "
"and put only the proven alternate form in aliases/title/full_name.\n"
- "- You may add a brand-new character only when the passage clearly introduces one that is not in the target roster.\n"
+ "- Do NOT invent brand-new profiles in this pass. Only refine the already-casted roster; ignore places, institutions, and other non-person entities.\n"
"- NEVER copy the known-characters roster into one character's aliases, relationships, title, or description fields.\n\n"
) if target_mode and known else ""
@@ -635,13 +635,17 @@ def _charsheets_prepare(data: dict) -> dict:
"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 "
- "if the book genuinely hasn't shown it yet (a later passage can fill it). Extrapolate from dialogue "
- "and actions when reasonable, and mark any deduced value with a trailing ' *'.\n"
+ "blank out a field you cannot improve. In this cast-character pass, only refine the already-casted "
+ "roster and do NOT invent new profiles, places, or institutions. The passage may be a focused "
+ "evidence window around a character mention, so use nearby paragraphs as context. Leave a field "
+ "empty if the book genuinely hasn't shown it yet (a later passage can fill it). Extrapolate from "
+ "dialogue and actions when reasonable, and mark any deduced value with a trailing ' *'.\n"
"For each character output these fields:\n"
"- name: canonical display name for this one character. Use the real personal name if known; otherwise use the most stable role/title.\n"
"- aliases: ONLY alternate names, roles, epithets, mistranscriptions, and titles proven to refer to the SAME character, comma-separated (max 6 items; e.g. 'Henker, Vampir, Zerwas der Henker'). Leave empty when uncertain.\n"
- "- first_name, last_name, full_name, title: split the character identity when known. Leave unknown parts empty. Put noble/office/role labels in title (e.g. 'Henker', 'Vampir', 'Graf').\n"
+ "- first_name, last_name, full_name: split the character identity when known. Leave unknown parts empty.\n"
+ "- title: nobility title only, if the text explicitly gives one (e.g. 'Graf', 'Baron', 'Ritter').\n"
+ "- profession: occupation / job / role in the story (e.g. 'Inquisitor', 'Soldier', 'Merchant', 'Priest').\n"
"- archetype: a two-word role summary (e.g. 'Ruthless Scholar')\n"
"- gender: 'male', 'female', or 'nonbinary' — as apparent from the text (pronouns, roles, physical description). Leave empty if genuinely indeterminable.\n"
"- physical: age, height, build, hair, eyes, skin, posture, gait, vocal quality. Use ONLY metric system.\n"
@@ -675,7 +679,7 @@ def _charsheets_prepare(data: dict) -> dict:
"Reuse the EXACT names from the known-characters list for returning characters when they are the canonical name or an alias of this character. "
"Do not merge characters merely because their names appear near each other, in the known-character list, or in relationships.\n"
"Respond with STRICT JSON only:\n"
- '{"sheets":[{"name":"","aliases":"","first_name":"","last_name":"","full_name":"","title":"","archetype":"","gender":"","physical":"","clothing":"",'
+ '{"sheets":[{"name":"","aliases":"","first_name":"","last_name":"","full_name":"","title":"","profession":"","archetype":"","gender":"","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":"","voice_design_prompt":"","image_prompt":"",'
@@ -1023,8 +1027,8 @@ async def character_generate_prompts(request: Request):
return str(v or "").strip()
profile = "\n".join(f"{label}: {v}" for label, v in [
- ("Name", name), ("Aliases/titles", _f("aliases") or _f("title")),
- ("Archetype", _f("archetype")), ("Physical", _f("physical")), ("Clothing", _f("clothing")),
+ ("Name", name), ("Aliases / noble title", _f("aliases") or _f("title")),
+ ("Occupation", _f("profession")), ("Archetype", _f("archetype")), ("Physical", _f("physical")), ("Clothing", _f("clothing")),
("Mannerisms", _f("mannerisms")), ("Voice/speech pattern", _f("voice_pattern")),
("Backstory", _f("backstory")), ("Motivation", _f("motivation")), ("Fears", _f("fears")),
("Relationships", _f("relationships")), ("Conflict style", _f("conflict_style")),
diff --git a/routes/reader.py b/routes/reader.py
index 3b35cb3..e0e7b36 100644
--- a/routes/reader.py
+++ b/routes/reader.py
@@ -286,7 +286,8 @@ def _md_to_cast(text: str) -> dict:
_CHAR_MD_SECTIONS = [
("Identity", [("Aliases / also known as", "aliases"), ("Full name", "full_name"),
- ("Title / role", "title"), ("Archetype", "archetype")]),
+ ("Title", "title"), ("Occupation", "profession"),
+ ("Archetype", "archetype")]),
("Appearance", [("Physical", "physical"), ("Clothing", "clothing")]),
("Personality", [("Mannerisms & habits", "mannerisms"), ("Voice & speech", "voice_pattern"),
("Motivation", "motivation"), ("Fears", "fears")]),
@@ -368,6 +369,7 @@ async def reader_list_scripts(doc_id: str):
scripts.append({
"name": f.stem,
"title": data.get("title", ""),
+ "profession": data.get("profession", ""),
"savedAt": data.get("savedAt"),
"segments": len(data.get("segments", [])),
"roster": data.get("roster", []),
diff --git a/static/index.html b/static/index.html
index f913522..50de865 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) {
-
+