LazyHuman10 commited on
Commit ·
1e98e8c
1
Parent(s): 1eea13b
fix: explicit JSON template for NPC gen + improved chat memory (20 turns)
Browse files- index.html +1 -1
- model_engine.py +72 -42
index.html
CHANGED
|
@@ -751,7 +751,7 @@
|
|
| 751 |
removeMessage(typingId);
|
| 752 |
appendDisplay("npc", response);
|
| 753 |
chatHistory.push({ role: "user", content: userText }, { role: "assistant", content: response });
|
| 754 |
-
chatHistory = chatHistory.slice(-
|
| 755 |
messageCount = new_msg_count;
|
| 756 |
unlockedSecrets = Array.from(new Set([...unlockedSecrets, ...newly_unlocked]));
|
| 757 |
updateFriendship(friendship_label, friendship_pct);
|
|
|
|
| 751 |
removeMessage(typingId);
|
| 752 |
appendDisplay("npc", response);
|
| 753 |
chatHistory.push({ role: "user", content: userText }, { role: "assistant", content: response });
|
| 754 |
+
chatHistory = chatHistory.slice(-40); // keep 40 messages for richer NPC memory
|
| 755 |
messageCount = new_msg_count;
|
| 756 |
unlockedSecrets = Array.from(new Set([...unlockedSecrets, ...newly_unlocked]));
|
| 757 |
updateFriendship(friendship_label, friendship_pct);
|
model_engine.py
CHANGED
|
@@ -263,49 +263,74 @@ def analyze_image(image_path: str) -> str:
|
|
| 263 |
|
| 264 |
def _npc_generation_prompt(description: str) -> str:
|
| 265 |
"""Build the primary JSON-only NPC generation prompt."""
|
| 266 |
-
return f"""
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
|
| 291 |
|
| 292 |
def _npc_retry_prompt(description: str) -> str:
|
| 293 |
"""Build a shorter strict prompt for retrying malformed JSON."""
|
| 294 |
-
return f"""
|
| 295 |
-
|
| 296 |
-
{description}
|
| 297 |
|
| 298 |
-
|
| 299 |
-
name,
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
-
|
| 303 |
-
passive_ability keys: name, description.
|
| 304 |
-
ultimate keys: name, description.
|
| 305 |
-
quests: exactly 3 objects with title, description, reward, rarity.
|
| 306 |
-
secrets: exactly 3 strings.
|
| 307 |
-
No markdown. No extra text.
|
| 308 |
-
""".strip()
|
| 309 |
|
| 310 |
|
| 311 |
@spaces.GPU
|
|
@@ -356,7 +381,7 @@ def _normalize_history(history: list) -> list[dict[str, str]]:
|
|
| 356 |
"""Convert common Gradio chat history formats into MiniCPM messages."""
|
| 357 |
normalized: list[dict[str, str]] = []
|
| 358 |
|
| 359 |
-
for exchange in history[-
|
| 360 |
if isinstance(exchange, dict):
|
| 361 |
role = exchange.get("role")
|
| 362 |
content = exchange.get("content")
|
|
@@ -389,8 +414,7 @@ def chat_respond(
|
|
| 389 |
friendship_label = get_friendship_label(msg_count)
|
| 390 |
passive = npc.get("passive_ability", DEFAULT_NPC["passive_ability"])
|
| 391 |
|
| 392 |
-
system_prompt = f"""
|
| 393 |
-
You are {npc_name}, an NPC in NPCverse. ALWAYS stay in character.
|
| 394 |
|
| 395 |
Name: {npc_name}
|
| 396 |
Class: {npc.get("class", DEFAULT_NPC["class"])}
|
|
@@ -403,9 +427,15 @@ Current friendship level: {friendship_label}
|
|
| 403 |
Unlocked secrets by index:
|
| 404 |
{_format_unlocked_secrets(npc, unlocked_secrets)}
|
| 405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
Respond naturally as this character. Keep replies concise, flavorful, and interactive.
|
| 407 |
-
Never say you are an AI model or break character.
|
| 408 |
-
""".strip()
|
| 409 |
|
| 410 |
msgs = [
|
| 411 |
{"role": "user", "content": [{"type": "text", "text": system_prompt}]},
|
|
|
|
| 263 |
|
| 264 |
def _npc_generation_prompt(description: str) -> str:
|
| 265 |
"""Build the primary JSON-only NPC generation prompt."""
|
| 266 |
+
return f"""You are the NPCverse character engine. Transform the visual description below into a vivid RPG character.
|
| 267 |
+
|
| 268 |
+
Return ONLY a valid JSON object. No backticks, no markdown, no extra text before or after.
|
| 269 |
+
|
| 270 |
+
You MUST include every single key shown in the template below. Do not skip any key.
|
| 271 |
+
|
| 272 |
+
JSON template (replace all placeholder values):
|
| 273 |
+
{{
|
| 274 |
+
"name": "<unique fantasy name inspired by appearance>",
|
| 275 |
+
"title": "<short evocative title, e.g. 'Wanderer of the Neon Wilds'>",
|
| 276 |
+
"class": "<creative RPG class name>",
|
| 277 |
+
"level": <integer 1-20>,
|
| 278 |
+
"rarity": "<one of: Common, Uncommon, Rare, Epic, Legendary>",
|
| 279 |
+
"alignment": "<e.g. Chaotic Good, Lawful Neutral, True Neutral>",
|
| 280 |
+
"lore": "<2-3 sentence backstory inspired by their appearance>",
|
| 281 |
+
"stats": {{
|
| 282 |
+
"strength": <1-100>,
|
| 283 |
+
"intelligence": <1-100>,
|
| 284 |
+
"charisma": <1-100>,
|
| 285 |
+
"luck": <1-100>,
|
| 286 |
+
"stealth": <1-100>,
|
| 287 |
+
"chaos": <1-100>
|
| 288 |
+
}},
|
| 289 |
+
"passive_ability": {{
|
| 290 |
+
"name": "<passive skill name>",
|
| 291 |
+
"description": "<what it does>"
|
| 292 |
+
}},
|
| 293 |
+
"ultimate": {{
|
| 294 |
+
"name": "<ultimate skill name>",
|
| 295 |
+
"description": "<what it does>"
|
| 296 |
+
}},
|
| 297 |
+
"weakness": "<one sentence describing their weakness or flaw>",
|
| 298 |
+
"faction": "<name of the group or faction they belong to>",
|
| 299 |
+
"world": "<name of the realm or world they come from>",
|
| 300 |
+
"opening_line": "<first thing they say when summoned, in character, 1-2 sentences>",
|
| 301 |
+
"quests": [
|
| 302 |
+
{{"title": "<quest 1 title>", "description": "<quest 1 desc>", "reward": "<item name>", "rarity": "Common"}},
|
| 303 |
+
{{"title": "<quest 2 title>", "description": "<quest 2 desc>", "reward": "<item name>", "rarity": "Rare"}},
|
| 304 |
+
{{"title": "<quest 3 title>", "description": "<quest 3 desc>", "reward": "<item name>", "rarity": "Epic"}}
|
| 305 |
+
],
|
| 306 |
+
"secrets": [
|
| 307 |
+
"<secret 1 about this character>",
|
| 308 |
+
"<secret 2 about this character>",
|
| 309 |
+
"<secret 3 about this character>"
|
| 310 |
+
],
|
| 311 |
+
"emoji": "<single emoji that represents this character>"
|
| 312 |
+
}}
|
| 313 |
+
|
| 314 |
+
Visual description to transform:
|
| 315 |
+
{description}""".strip()
|
| 316 |
|
| 317 |
|
| 318 |
def _npc_retry_prompt(description: str) -> str:
|
| 319 |
"""Build a shorter strict prompt for retrying malformed JSON."""
|
| 320 |
+
return f"""Output ONLY a valid JSON object. No markdown. No extra text.
|
| 321 |
+
|
| 322 |
+
Create an RPG character from this description: {description}
|
| 323 |
|
| 324 |
+
Required JSON — include ALL these keys:
|
| 325 |
+
{{"name":"","title":"","class":"","level":1,"rarity":"Common","alignment":"","lore":"",
|
| 326 |
+
"stats":{{"strength":50,"intelligence":50,"charisma":50,"luck":50,"stealth":50,"chaos":50}},
|
| 327 |
+
"passive_ability":{{"name":"","description":""}},
|
| 328 |
+
"ultimate":{{"name":"","description":""}},
|
| 329 |
+
"weakness":"","faction":"","world":"","opening_line":"",
|
| 330 |
+
"quests":[{{"title":"","description":"","reward":"","rarity":"Common"}},{{"title":"","description":"","reward":"","rarity":"Rare"}},{{"title":"","description":"","reward":"","rarity":"Epic"}}],
|
| 331 |
+
"secrets":["","",""],"emoji":"⚔"}}
|
| 332 |
|
| 333 |
+
Fill in all empty string values with creative content based on the description.""".strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
|
| 335 |
|
| 336 |
@spaces.GPU
|
|
|
|
| 381 |
"""Convert common Gradio chat history formats into MiniCPM messages."""
|
| 382 |
normalized: list[dict[str, str]] = []
|
| 383 |
|
| 384 |
+
for exchange in history[-20:]: # keep last 20 turns for richer memory
|
| 385 |
if isinstance(exchange, dict):
|
| 386 |
role = exchange.get("role")
|
| 387 |
content = exchange.get("content")
|
|
|
|
| 414 |
friendship_label = get_friendship_label(msg_count)
|
| 415 |
passive = npc.get("passive_ability", DEFAULT_NPC["passive_ability"])
|
| 416 |
|
| 417 |
+
system_prompt = f"""You are {npc_name}, an NPC in NPCverse. ALWAYS stay in character.
|
|
|
|
| 418 |
|
| 419 |
Name: {npc_name}
|
| 420 |
Class: {npc.get("class", DEFAULT_NPC["class"])}
|
|
|
|
| 427 |
Unlocked secrets by index:
|
| 428 |
{_format_unlocked_secrets(npc, unlocked_secrets)}
|
| 429 |
|
| 430 |
+
IMPORTANT memory rules:
|
| 431 |
+
- You have access to the full conversation history below. Read it carefully.
|
| 432 |
+
- Remember everything the user has told you (their name, choices, quests, preferences).
|
| 433 |
+
- Reference past exchanges naturally — if they told you their name, use it; if they completed a quest, acknowledge it.
|
| 434 |
+
- Never contradict something you said earlier in this conversation.
|
| 435 |
+
- Stay consistent with your established personality throughout.
|
| 436 |
+
|
| 437 |
Respond naturally as this character. Keep replies concise, flavorful, and interactive.
|
| 438 |
+
Never say you are an AI model or break character.""".strip()
|
|
|
|
| 439 |
|
| 440 |
msgs = [
|
| 441 |
{"role": "user", "content": [{"type": "text", "text": system_prompt}]},
|