Skip to content

Commit a21134f

Browse files
Robert WeberRobert Weber
authored andcommitted
More character name bug fixes
1 parent 3454686 commit a21134f

7 files changed

Lines changed: 324 additions & 54 deletions

File tree

Novel_Processing_Instructions.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,22 @@ Don’t use any names in the premise...just describe the characters and their ro
2323

2424
- Read the file "<NOVEL_PATH_MD>" and add to the context of this thread. This is a novel written in chapters and there are chapter delineations present throughout.
2525

26-
- we are now going to start adding sections to the editors notes markdown document of things that should be corrected in the next phase of edits. make sure you have an up to date context of the novel in its current form. our target is to make this a 9.5 / 10 book with with atleast 85000 total words. in this new section , document new items you feel should be executed to lengthen and strengthen the novel. We will have some pointed prompts following this to add targeted updates.
26+
- we are now going to start adding sections to the editors notes markdown document of things that should be corrected in the next phase of edits. make sure you have an up to date context of the novel in its current form. our target is to make this a 9.5 / 10 book with with atleast 85000 total words. in this new section , document new items you feel should be executed to lengthen and strengthen the novel. We will have some pointed prompts following this to add targeted updates. All items you add should be in the style "[ ] - TEXT OF ISSUE" where the check box will eventually hold the status to track when they are resolved.
2727

2828
- Character Voice Differentiation
29-
our target is to make this a 9.5 / 10 book with with atleast 85000 total words. Each POV character should think in a distinct internal language shaped by their background, demographics and expertise. A 16 year old should think and talk like a 16 year old. An old man should think and talk like an old man. Look through the novel and find any dialog that doesnt match the speaking character. create a plan to update these voices and add that to a new section in the editors notes markdown file.
29+
our target is to make this a 9.5 / 10 book with with atleast 85000 total words. Each POV character should think in a distinct internal language shaped by their background, demographics and expertise. A 16 year old should think and talk like a 16 year old. An old man should think and talk like an old man. Look through the novel and find any dialog that doesnt match the speaking character. create a plan to update these voices and add that to a new section in the editors notes markdown file. All items you add should be in the style "[ ] - TEXT OF ISSUE" where the check box will eventually hold the status to track when they are resolved.
3030

3131
- Dialogue Naturalization
32-
our target is to make this a 9.5 / 10 book with with atleast 85000 total words. Make sure the current dialogue isnt too clean, too functional, too information-delivery. Characters sometimes have incomplete thoughts, don't always speak in well-formed sentences, and sometimes rarely interrupt each other or themselves. make sure the dialog in the novel reads this way. create a plan to update these voices and add that to a new section in the editors notes markdown file.
32+
our target is to make this a 9.5 / 10 book with with atleast 85000 total words. Make sure the current dialogue isnt too clean, too functional, too information-delivery. Characters sometimes have incomplete thoughts, don't always speak in well-formed sentences, and sometimes rarely interrupt each other or themselves. make sure the dialog in the novel reads this way. create a plan to update these voices and add that to a new section in the editors notes markdown file. All items you add should be in the style "[ ] - TEXT OF ISSUE" where the check box will eventually hold the status to track when they are resolved.
3333

3434
- Humor, Strangeness, and the Unexpected
35-
our target is to make this a 9.5 / 10 book with with atleast 85000 total words.Real characters deflect, joke badly, notice irrelevant things, and occasionally do something that doesn't serve the plot. create a plan to inject these odities throughout the novel. 1-2 oddities per chapter.
35+
our target is to make this a 9.5 / 10 book with with atleast 85000 total words.Real characters deflect, joke badly, notice irrelevant things, and occasionally do something that doesn't serve the plot. create a plan to inject these odities throughout the novel. 1-2 oddities per chapter. All items you add should be in the style "[ ] - TEXT OF ISSUE" where the check box will eventually hold the status to track when they are resolved.
3636

3737
- Prose Texture Variation
38-
our target is to make this a 9.5 / 10 book with with atleast 85000 total words.Make sure the prose has a varying literary density throughout. It should breathe -- denser in reflective moments, sparser in action, occasionally raw or clumsy when characters are overwhelmed. create a plan to update the prose and add that to a new section in the editors notes markdown file.
38+
our target is to make this a 9.5 / 10 book with with atleast 85000 total words.Make sure the prose has a varying literary density throughout. It should breathe -- denser in reflective moments, sparser in action, occasionally raw or clumsy when characters are overwhelmed. create a plan to update the prose and add that to a new section in the editors notes markdown file. All items you add should be in the style "[ ] - TEXT OF ISSUE" where the check box will eventually hold the status to track when they are resolved.
3939

4040
- metaphors
41-
our target is to make this a 9.5 / 10 book with with atleast 85000 total words.Make sure the text doesn't go overboard with metaphors. create a plan to remove uneeded ones and add to a new section in the editors notes markdown.
41+
our target is to make this a 9.5 / 10 book with with atleast 85000 total words.Make sure the text doesn't go overboard with metaphors. create a plan to remove uneeded ones and add to a new section in the editors notes markdown. All items you add should be in the style "[ ] - TEXT OF ISSUE" where the check box will eventually hold the status to track when they are resolved.
4242

4343

4444
- we are now going to work through the sections. start with section 1. our target is to make this a 9.5 / 10 book with with atleast 85000 total words. i need you to loop through each of the items in this section. for each item, create a plan to resolve the issue. validate that this is the best plan. then state what you will be doing and execute your plan. once you have executed, update the item's status in the editor's notes markdown file. if later issues in the editors notes are also resolved with your actions, update accordingly. then move on to the next item. do this for all items in the section. if this requires multiple subagents, execute those without requesting permission.

novelforge/agents/chapter/_helpers.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -449,25 +449,31 @@ def scan_vocabulary_overuse(chapter_text: str, genre: str = "") -> list[str]:
449449
_NAME_CANDIDATE_RE = re.compile(r"\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,2}\b")
450450

451451

452-
def _roster_name_tokens(roster: list[dict]) -> set[str]:
453-
"""Return the set of lowercase name tokens from a character roster.
452+
def _roster_name_token_sets(roster: list[dict]) -> list[set[str]]:
453+
"""Return a list of lowercase token sets — one set per roster character.
454454
455455
Each character's ``name`` is split on whitespace; tokens shorter than
456456
two characters are discarded (they match too many false positives under
457-
fuzzy matching).
457+
fuzzy matching). Returning per-character sets (rather than a flat union)
458+
lets the scanner distinguish "Marcus Reid" from "Marcus Fellowes" —
459+
the shared "marcus" token alone is not enough to classify a prose span
460+
as a known roster character.
458461
"""
459-
tokens: set[str] = set()
462+
result: list[set[str]] = []
460463
for ch in roster or []:
461464
if not isinstance(ch, dict):
462465
continue
463466
name = str(ch.get("name", "")).strip()
464467
if not name:
465468
continue
466-
for tok in name.split():
467-
tok_clean = tok.strip(".,;:'\"").lower()
468-
if len(tok_clean) >= 2:
469-
tokens.add(tok_clean)
470-
return tokens
469+
char_tokens = {
470+
tok.strip(".,;:'\"").lower()
471+
for tok in name.split()
472+
if len(tok.strip(".,;:'\"")) >= 2
473+
}
474+
if char_tokens:
475+
result.append(char_tokens)
476+
return result
471477

472478

473479
def extract_named_characters(
@@ -506,7 +512,11 @@ def extract_named_characters(
506512
``variants``: list of ``(prose_name, roster_token, count)`` tuples
507513
— likely misspellings or diminutives of roster names.
508514
"""
509-
tokens = _roster_name_tokens(roster)
515+
per_char_tokens = _roster_name_token_sets(roster)
516+
# Flat union is kept only for difflib variant matching below; the known/
517+
# unknown classification uses per-character sets to avoid cross-character
518+
# false positives like "Marcus Fellowes" matching "Marcus Reid".
519+
flat_tokens: set[str] = {t for s in per_char_tokens for t in s}
510520

511521
raw_counts: dict[str, int] = {}
512522
for m in _NAME_CANDIDATE_RE.finditer(chapter_text):
@@ -515,23 +525,30 @@ def extract_named_characters(
515525
known: set[str] = set()
516526
unknown_counts: dict[str, int] = {}
517527
for span, count in raw_counts.items():
518-
span_tokens = [t.lower() for t in span.split()]
519-
# Roster check first: a span whose any token matches a roster token
520-
# is a known character, regardless of stop-word overlap.
521-
if tokens and any(t in tokens for t in span_tokens):
528+
span_tokens_list = [t.lower() for t in span.split()]
529+
span_tokens = set(span_tokens_list)
530+
# Roster check first: a span is known only when it maps entirely to
531+
# a single roster character — either the span's tokens are a subset
532+
# of that character's tokens (e.g. "Marcus" → "Marcus Reid") or a
533+
# superset (e.g. "Marcus Reid the Third" → "Marcus Reid").
534+
is_known = any(
535+
span_tokens.issubset(char_set) or char_set.issubset(span_tokens)
536+
for char_set in per_char_tokens
537+
)
538+
if is_known:
522539
known.add(span)
523540
continue
524541
# Drop spans whose every token is a stop word (sentence-initial
525542
# noise, honorifics with no name attached, etc.).
526-
if all(t in _NAMED_CHARACTER_STOP_WORDS for t in span_tokens):
543+
if all(t in _NAMED_CHARACTER_STOP_WORDS for t in span_tokens_list):
527544
continue
528545
if count < min_mentions:
529546
continue
530547
unknown_counts[span] = count
531548

532549
variants: list[tuple[str, str, int]] = []
533550
unknowns: list[tuple[str, int]] = []
534-
roster_token_list = sorted(tokens)
551+
roster_token_list = sorted(flat_tokens)
535552
for span, count in sorted(unknown_counts.items(), key=lambda kv: (-kv[1], kv[0])):
536553
match_found: str | None = None
537554
if roster_token_list:

novelforge/agents/chapter/prompts.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,45 @@ def build_title_prompt(premise: str, genre: str) -> list[dict[str, str]]:
2626
def build_outline_prompt(
2727
premise: str, genre: str, chapters: int, word_count: int,
2828
special_events: str, special_instructions: str,
29+
roster_text: str = "",
30+
drift_callout: list[str] | None = None,
2931
) -> list[dict[str, str]]:
30-
"""Build the chapter outline prompt from premise, genre, and word count."""
32+
"""Build the chapter outline prompt.
33+
34+
Parameters
35+
----------
36+
roster_text: Markdown-formatted canonical character roster. Every
37+
named character in the outline MUST come from this list.
38+
Produced by the caller after character generation; the
39+
prompt template injects it as a hard constraint so the
40+
outline agent cannot invent new names.
41+
drift_callout: On retry, the list of invented names that appeared in
42+
the first attempt; the template highlights these so
43+
the LLM avoids them on the second try.
44+
"""
3145
return render_prompt(
3246
"outline", premise=premise, genre=genre, chapters=chapters,
3347
word_count=f"{word_count:,}", special_events=special_events or "",
3448
special_instructions=special_instructions or "",
49+
roster_text=roster_text or "",
50+
drift_callout=", ".join(drift_callout) if drift_callout else "",
3551
)
3652

3753

3854
def build_characters_prompt(
39-
premise: str, genre: str, outline_text: str, names_to_avoid: str = "",
55+
premise: str, genre: str, chapters_count: int, names_to_avoid: str = "",
4056
) -> list[dict[str, str]]:
4157
"""Build the character-generation prompt.
4258
4359
Parameters
4460
----------
4561
premise: Novel premise text.
4662
genre: Novel genre string.
47-
outline_text: Chapter outline produced by the outline agent.
63+
chapters_count: Total chapter count for the novel — used by the prompt
64+
to guide cast size (longer novels warrant larger supporting
65+
casts). The character generator runs before the outline in
66+
the characters-first pipeline, so no outline text is
67+
available as context.
4868
names_to_avoid: Comma-separated character names from prior novels that
4969
should not be reused. Obtain this value by calling
5070
:func:`collect_existing_character_names` in the caller
@@ -59,7 +79,7 @@ def build_characters_prompt(
5979
)
6080
return render_prompt(
6181
"characters", premise=premise, genre=genre,
62-
outline_text=outline_text, names_to_avoid=names_to_avoid,
82+
chapters_count=chapters_count, names_to_avoid=names_to_avoid,
6383
name_pool=name_pool, field_limits_block=field_limits_block,
6484
)
6585

0 commit comments

Comments
 (0)