fix: apply the director's character attribute limit to character card import (#166)

* fix: apply the director's character attribute limit to character card import

* fix: address PR review feedback on card import attribute limit

* fix: trim docs-index summary to avoid find_docs false positives
This commit is contained in:
veguAI
2026-07-28 00:24:47 +03:00
committed by GitHub
parent 53ecfe5029
commit 3eb14f8c0a
6 changed files with 116 additions and 6 deletions

View File

@@ -32,6 +32,7 @@
fixes:
- "Character Card Import: A failed description or example dialogue generation no longer wipes the card's own text. Both steps clear the field before regenerating it so the card's version doesn't bias the rewrite, but a generation that errored out or came back empty left the character with nothing at all — an empty description then also blinded the attribute extraction that runs right after it. The card's original description and example dialogue are now restored whenever generation fails, returns nothing, or is cancelled, matching what the import already does when those generation steps are switched off."
- "Character Card Import: A failed attribute extraction no longer overwrites the card's attributes. A generation that errored out or came back empty was still written to the character — as a sheet containing nothing but the character's own name — which dropped whatever attributes the card supplied (its gender, for example) and shadowed the character description in every prompt that renders the character sheet. An extraction that produces nothing now keeps the card's attributes, matching what the import already does when the attribute step is switched off."
- "Character Card Import: The director's 'Limit character attributes' setting now applies to character card import. The setting governs every other character creation path the director manages, but import ignored it entirely — importing a card with a limit of 5 configured still produced an unbounded sheet, with nothing indicating why. Both import paths (the per-aspect flow and the Creator's Fast one-shot) now generate at most the configured number of attributes. Attributes the card itself supplies are untouched when attribute extraction is switched off."
- "Pocket TTS: Pasting a Hugging Face token into the agent settings now enables the gated voice-cloning model download without restarting Talemate — previously a generation attempted before the token was set cached the fallback model without voice cloning, so following the in-app 'paste token and try again' instructions kept failing until a restart. While voice cloning is unavailable the agent card now also shows a warning."
- "Prompt Templates: The `system_time` template function crashed with 'Invalid format string' on Windows — it used strftime codes only available on Linux/macOS. The time is now formatted platform-independently with identical output."
- "Director Chat: Images created through the director chat's image generation action never appeared in the chat — the generation completed and the image was saved to the scene's assets, but the 'Image Generated' message was inserted into a newly created orphan chat instead of the conversation that requested it. The message is now inserted into the initiating chat."

View File

@@ -159,7 +159,7 @@ The Character Management settings control how the director handles character cre
##### Limit character attributes
Controls the maximum number of attributes that will be generated when creating or updating character sheets. This applies when the director creates new characters or when character sheets are generated through templates.
Controls the maximum number of attributes that will be generated when creating or updating character sheets. This applies when the director creates new characters, when character sheets are generated through templates, and when a character sheet is generated during [character card import](../../character-card-import.md).
- **0** (default): No limit - attributes are generated without restriction
- **1-40**: Generates at most this many attributes (the character's own name is not counted)

View File

@@ -113,7 +113,7 @@ By default, importing a character card runs several AI generation steps that enr
- **Generate Description**: AI rewrites the character description. When disabled, the card's original description is kept as-is.
- **Extract Attributes**: AI extracts a structured attribute sheet (age, appearance, personality, etc.) from the card.
- **Extract Attributes**: AI extracts a structured attribute sheet (age, appearance, personality, etc.) from the card. The Director's [**Limit character attributes**](/talemate/user-guide/agents/director/settings/#limit-character-attributes) setting caps how many attributes this generates.
- **Generate Dialogue Instructions**: AI generates acting instructions that guide how the character speaks and behaves.

View File

@@ -385,9 +385,9 @@
title: Character Card Import
summary: How to import TavernAI-style character cards (Chara Card V0-V3, PNG/JPG/WebP images or JSON) from the home screen's
Import dropzone or the Scene Library's Character Cards section to create a new scene. Covers the import dialog, character
detection/manual selection, options for character book entries, alternate greetings, shared context setup, writing style
template, player character setup, and troubleshooting
failed analysis.
detection/manual selection, the AI Generation section (per-step generation toggles with card-data fallback, Full/Minimal
presets, and the director's attribute limit capping extracted attributes), options for character book entries, alternate
greetings, shared context setup, writing style template, player character setup, and troubleshooting failed analysis.
- path: user-guide/clients/auto-retry.md
title: Auto Retry
summary: 'Per-client sliders (0-5, default 0 = notify immediately) for automatically retrying response issues before the

View File

@@ -681,6 +681,7 @@ async def _determine_character_attributes(
character,
loading_status: LoadingStatus,
relevant_info: RelevantCharacterInfo,
max_attrs: int | None = None,
) -> None:
"""Determine and set character attributes."""
loading_status("Determine character attributes...")
@@ -693,6 +694,7 @@ async def _determine_character_attributes(
# context character the prompt's active-character loop misses it
character=character,
dynamic_instructions=relevant_info.to_dynamic_instructions(scenario=False),
max_attributes=max_attrs,
)
# an extraction that produced nothing must not replace the card's
@@ -1303,6 +1305,7 @@ async def _process_character_fast(
original_dialogue_examples_text: str,
import_options: CharacterCardImportOptions,
max_examples: int = 5,
max_attrs: int | None = None,
) -> None:
"""Fast mode: route the enabled extraction aspects through the creator
agent's consolidated generation (one prompt instead of one per aspect).
@@ -1321,6 +1324,7 @@ async def _process_character_fast(
original_dialogue_examples_text: Original dialogue examples text from character card
import_options: Import options
max_examples: Maximum number of dialogue examples to generate (default: 5)
max_attrs: Maximum number of attributes to generate (None for unlimited)
"""
creator = instance.get_agent("creator")
@@ -1363,6 +1367,7 @@ async def _process_character_fast(
scenario=False
),
max_examples=max_examples,
max_attributes=max_attrs,
content_role="text",
)
)
@@ -1426,6 +1431,9 @@ async def _process_characters_for_import(
director = instance.get_agent("director")
creator = instance.get_agent("creator")
# 0 means unlimited
max_attrs = director.cm_max_attributes or None
for character in characters:
# Add character to character_data without activating
# Characters will be activated later by _activate_characters_from_greeting
@@ -1450,6 +1458,7 @@ async def _process_characters_for_import(
relevant_info=relevant_info,
original_dialogue_examples_text=original_dialogue_examples_text,
import_options=import_options,
max_attrs=max_attrs,
)
else:
if import_options.extract_description:
@@ -1461,7 +1470,10 @@ async def _process_characters_for_import(
if import_options.extract_attributes:
await _determine_character_attributes(
character, loading_status, relevant_info=relevant_info
character,
loading_status,
relevant_info=relevant_info,
max_attrs=max_attrs,
)
if import_options.extract_dialogue_instructions:

View File

@@ -84,6 +84,14 @@ def fast_mode(agents):
agents.creator.actions["character_creation"].config["fast"].value = False
@pytest.fixture
def max_attributes(agents):
config = agents.director.actions["character_management"].config["max_attributes"]
previous = config.value
yield lambda value: setattr(config, "value", value)
config.value = previous
def _scene_stub():
return SimpleNamespace(
name=None,
@@ -801,6 +809,95 @@ async def test_process_characters_fast_mode_cancellation_propagates(fast_mode):
assert character.example_dialogue == ["Hero: raw example"]
# ---------------------------------------------------------------------------
# Director attribute limit
# ---------------------------------------------------------------------------
async def test_process_characters_split_mode_sheet_capped_by_max_attributes(
agents, max_attributes
):
max_attributes(2)
character = _card_character()
scene = _scene_stub()
async with MockClientContext():
client_responses.get().append(
sheet_response(
"Hero", {"Age": "20", "Occupation": "knight", "Mood": "grim"}
)
)
await _process_characters_for_import(
scene,
[character],
["hi"],
"",
LoadingStatus(None),
_options_all_disabled(extract_attributes=True),
)
# enforced twice: prompt instruction + parser truncation
assert "at most 2 attributes" in str(agents.client.prompt_history[0]["prompt"])
assert character.base_attributes == {"Name": "Hero", "Age": "20"}
async def test_process_characters_split_mode_zero_max_attributes_uncapped(agents):
# 0 means unlimited - the default - and must not reach the prompt
assert agents.director.cm_max_attributes == 0
character = _card_character()
scene = _scene_stub()
async with MockClientContext():
client_responses.get().append(
sheet_response(
"Hero", {"Age": "20", "Occupation": "knight", "Mood": "grim"}
)
)
await _process_characters_for_import(
scene,
[character],
["hi"],
"",
LoadingStatus(None),
_options_all_disabled(extract_attributes=True),
)
assert "at most" not in str(agents.client.prompt_history[0]["prompt"])
assert character.base_attributes == {
"Name": "Hero",
"Age": "20",
"Occupation": "knight",
"Mood": "grim",
}
async def test_process_characters_fast_mode_capped_by_max_attributes(
fast_mode, max_attributes
):
agents = fast_mode
max_attributes(2)
character = _card_character()
scene = _scene_stub()
async with MockClientContext():
client_responses.get().append(
unified_response(
attributes="Age: 20\nOccupation: knight\nMood: grim",
)
)
await _process_characters_for_import(
scene,
[character],
["hi"],
"",
LoadingStatus(None),
_options_all_disabled(extract_attributes=True),
)
assert "At most 2 attributes" in str(agents.client.prompt_history[0]["prompt"])
assert character.base_attributes == {"Age": "20", "Occupation": "knight"}
@pytest.mark.parametrize(
"aspect,agent_attr,method",
[